cfxReconMode = {} cfxReconMode.version = "2.2.2" cfxReconMode.verbose = false -- set to true for debug info cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered cfxReconMode.prioList = {} -- group names that are high prio and generate special event cfxReconMode.blackList = {} -- group names that are NEVER detected. Comma separated strings, e.g. {"Always Hidden", "Invisible Group"} cfxReconMode.dynamics = {} -- if a group name is dynamic cfxReconMode.zoneInfo = {} -- additional zone info cfxReconMode.scoutZones = {} -- zones that define aircraft. used for late eval of players cfxReconMode.allowedScouts = {} -- when not using autoscouts cfxReconMode.blindScouts = {} -- to exclude aircraft from being scouts cfxReconMode.removeWhenDestroyed = true cfxReconMode.activeMarks = {} -- all marks and their groups, indexed by groupName cfxReconMode.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } cfxReconMode.name = "cfxReconMode" -- to be compatible with test flags --[[-- VERSION HISTORY 2.0.0 - DML integration prio+-->prio! detect+ --> detect! and method - changed access to prio and blacklist to hash - dynamic option for prio and black - trigger zones for designating prio and blacklist - reworked stringInList to also include dynamics - Report in SALT format: size, action, loc, time. - Marks add size, action info - LatLon or MGRS - MGRS option in config - filter onEvent for helo and aircraft - allowedScouts and blind - stronger scout filtering at startup - better filtering on startup when autorecon and playeronly - player lazy late checking, zone saving - correct checks when not autorecon - ability to add special flags to recon prio group - event guard in onEvent - wildcard - , , wildcards 2.0.1 - getGroup() guard for onEvent(). Objects now seem to birth. 2.1.0 - processZoneMessage uses group's position, not zone - silent attribute for priority targets - activate / deactivate by flags 2.1.1 - Lat Lon and MGRS also give Elevation - cfxReconMode.reportTime 2.1.2 - imperialUnits for elevation - wildcard in message format - fix for mgrs bug in message (zone coords, not unit) 2.1.3 - added cfxReconMode.name to allow direct acces with test zone flag 2.1.4 - canDetect() also checks if unit has been activated canDetect has strenghtened isExist() guard 2.2.0 - new marksLocked config attribute, defaults to false - new marksFadeAfter config attribute to control mark time - dmlZones OOP upgrade 2.2.1 - fixed "cfxReconSMode" typo 2.2.2 - added groupNames attribute - clean-up --]]-- cfxReconMode.detectionMinRange = 3000 -- meters at ground level cfxReconMode.detectionMaxRange = 12000 -- meters at max alt (10'000m) cfxReconMode.maxAlt = 9000 -- alt for maxrange (9km = 27k feet) cfxReconMode.autoRecon = true -- add all airborne units, unless cfxReconMode.redScouts = false -- set to false to prevent red scouts in auto mode cfxReconMode.blueScouts = true -- set to false to prevent blue scouts in auto-mode cfxReconMode.greyScouts = false -- set to false to prevent neutral scouts in auto mode cfxReconMode.playerOnlyRecon = false -- only players can do recon cfxReconMode.reportNumbers = true -- also add unit count in report cfxReconMode.prioFlag = nil cfxReconMode.detectFlag = nil cfxReconMode.method = "inc" cfxReconMode.applyMarks = true cfxReconMode.mgrs = false cfxReconMode.ups = 1 -- updates per second. cfxReconMode.scouts = {} -- units that are performing scouting. cfxReconMode.processedScouts = {} -- for managing performance: queue cfxReconMode.detectedGroups = {} -- so we know which have been detected cfxReconMode.marksFadeAfter = 30*60 -- after detection, marks disappear after -- this amount of seconds. -1 means no fade -- 60 is one minute cfxReconMode.callbacks = {} -- sig: cb(reason, side, scout, group) cfxReconMode.uuidCount = 0 -- for unique marks function cfxReconMode.uuid() cfxReconMode.uuidCount = cfxReconMode.uuidCount + 1 return cfxReconMode.uuidCount end function cfxReconMode.addCallback(theCB) table.insert(cfxReconMode.callbacks, theCB) end function cfxReconMode.invokeCallbacks(reason, theSide, theScout, theGroup, theName) for idx, theCB in pairs(cfxReconMode.callbacks) do theCB(reason, theSide, theScout, theGroup, theName) end end -- add a priority/blackList group name to prio list function cfxReconMode.addToPrioList(aGroup, dynamic) if not dynamic then dynamic = false end if not aGroup then return end if type(aGroup) == "table" and aGroup.getName then aGroup = aGroup:getName() end if type(aGroup) == "string" then cfxReconMode.prioList[aGroup] = aGroup cfxReconMode.dynamics[aGroup] = dynamic end end function cfxReconMode.addToBlackList(aGroup, dynamic) if not dynamic then dynamic = false end if not aGroup then return end if type(aGroup) == "table" and aGroup.getName then aGroup = aGroup:getName() end if type(aGroup) == "string" then cfxReconMode.blackList[aGroup] = aGroup cfxReconMode.dynamics[aGroup] = dynamic end end function cfxReconMode.addToAllowedScoutList(aGroup, dynamic) if not dynamic then dynamic = false end if not aGroup then return end if type(aGroup) == "table" and aGroup.getName then aGroup = aGroup:getName() end if type(aGroup) == "string" then cfxReconMode.allowedScouts[aGroup] = aGroup cfxReconMode.dynamics[aGroup] = dynamic end end function cfxReconMode.addToBlindScoutList(aGroup, dynamic) if not dynamic then dynamic = false end if not aGroup then return end if type(aGroup) == "table" and aGroup.getName then aGroup = aGroup:getName() end if type(aGroup) == "string" then cfxReconMode.blindScouts[aGroup] = aGroup cfxReconMode.dynamics[aGroup] = dynamic end end function cfxReconMode.isStringInList(theString, theList) -- returns two values: inList, and original group name (if exist) if not theString then return false, nil end if type(theString) ~= "string" then return false, nil end if not theList then return false, nil end -- first, try a direct look-up. if this produces a hit -- we directly return true if theList[theString] then return true, theString end -- now try the more involved retrieval with string starts with for idx, aName in pairs(theList) do if dcsCommon.stringStartsWith(theString, aName) then -- they start the same. are dynamics allowed? if cfxReconMode.dynamics[aName] then return true, aName end end end return false, nil end -- since we use a queue for scouts, also always check the -- processed queue before adding to make sure a scout isn't -- entered multiple times function cfxReconMode.addScout(theUnit) if not theUnit then trigger.action.outText("+++cfxRecon: WARNING - nil Unit on add", 30) return end if type(theUnit) == "string" then local u = Unit.getByName(theUnit) theUnit = u end if not theUnit then trigger.action.outText("+++cfxRecon: WARNING - did not find unit on add", 30) return end if not theUnit:isExist() then return end -- find out if this an update or a new scout local thisID = tonumber(theUnit:getID()) local theName = theUnit:getName() local lastUnit = cfxReconMode.scouts[theName] local isProcced = false -- may also be in procced line if not lastUnit then lastUnit = cfxReconMode.processedScouts[theName] if lastUnit then isProcced = true end end if lastUnit then -- this is merely an overwrite if cfxReconMode.verbose then trigger.action.outText("+++rcn: UPDATE scout " .. theName .. " -- no CB invoke", 30) end else if cfxReconMode.verbose then trigger.action.outText("+++rcn: new scout " .. theName .. " with ID " .. thisID, 30) end -- a new scout! Invoke callbacks local scoutGroup = theUnit:getGroup() local theSide = scoutGroup:getCoalition() cfxReconMode.invokeCallbacks("start", theSide, theUnit, nil, "") end if isProcced then -- overwrite exiting entry in procced queue cfxReconMode.processedScouts[theName] = theUnit else -- add / overwrite into normal queue cfxReconMode.scouts[theName] = theUnit end if cfxReconMode.verbose then trigger.action.outText("+++rcn: addded scout " .. theUnit:getName(), 30) end end function cfxReconMode.removeScout(theUnit) if not theUnit then trigger.action.outText("+++rcn: WARNING - nil Unit on remove", 30) return end if type(theUnit) == "string" then cfxReconMode.removeScoutByName(theUnit) return end if not theUnit then return end if not theUnit:isExist() then return end cfxReconMode.removeScoutByName(theUnit:getName()) local scoutGroup = theUnit:getGroup() local theSide = scoutGroup:getCoalition() cfxReconMode.invokeCallbacks("end", theSide, theUnit, nil, "") end -- warning: removeScoutByName does NOT invoke callbacks, always -- use removeScout instead! function cfxReconMode.removeScoutByName(aName) cfxReconMode.scouts[aName] = nil cfxReconMode.processedScouts[aName] = nil -- also remove from processed stack if cfxReconMode.verbose then trigger.action.outText("+++rcn: removed scout " .. aName, 30) end end function cfxReconMode.canDetect(scoutPos, theGroup, visRange) -- determine if a member of theGroup can be seen from -- scoutPos at visRange -- returns true and pos when detected local allUnits = theGroup:getUnits() for idx, aUnit in pairs(allUnits) do if Unit.isExist(aUnit) and aUnit:isActive() and aUnit:getLife() >= 1 then local uPos = aUnit:getPoint() uPos.y = uPos.y + 3 -- raise my 3 meters local d = dcsCommon.distFlat(scoutPos, uPos) if d < visRange then -- is in visual range. do we have LOS? if land.isVisible(scoutPos, uPos) then -- group is visible, stop here, return true return true, uPos end else -- OPTIMIZATION: if a unit is outside -- detect range, we assume that entire group -- is, since they are bunched together -- edge cases may get lucky tests return false, nil end end end return false, nil -- nothing visible end function cfxReconMode.placeMarkForUnit(location, theSide, theGroup) local theID = cfxReconMode.uuid() local theDesc = "Contact" if cfxReconMode.groupNames then theDesc = theDesc .. ": " ..theGroup:getName() end if cfxReconMode.reportNumbers then theDesc = theDesc .. " - " .. cfxReconMode.getSit(theGroup) .. ", " .. cfxReconMode.getAction(theGroup) .. "." end trigger.action.markToCoalition( theID, theDesc, location, theSide, cfxReconMode.marksLocked, -- readOnly -- false, nil) return theID end function cfxReconMode.removeMarkForArgs(args) local theSide = args[1] local theScout = args[2] local theGroup = args[3] local theID = args[4] local theName = args[5] -- only remove if it wasn't already removed. -- this method is called async *and* sync! if cfxReconMode.activeMarks[theName] then trigger.action.removeMark(theID) -- invoke callbacks cfxReconMode.invokeCallbacks("removed", theSide, theScout, theGroup, theName) cfxReconMode.activeMarks[theName] = nil -- also remove from list of groups being checked end cfxReconMode.detectedGroups[theName] = nil -- some housekeeping. end function cfxReconMode.getSit(theGroup) local msg = "" -- analyse the group we just discovered. We know it's a ground troop, so simply differentiate between vehicles and infantry local theUnits = theGroup:getUnits() local numInf = 0 local numVehicles = 0 for idx, aUnit in pairs(theUnits) do if dcsCommon.unitIsInfantry(aUnit) then numInf = numInf + 1 else numVehicles = numVehicles + 1 end end if numInf > 0 and numVehicles > 0 then -- mixed infantry and vehicles msg = numInf .. " infantry and " .. numVehicles .. " vehicles" elseif numInf > 0 then -- only infantry msg = numInf .. " infantry" else -- only vehicles msg = numVehicles .. " vehicles" end return msg end function cfxReconMode.getAction(theGroup) local msg = "" -- simply get the first unit and get velocity vector. -- if it's smaller than 1 m/s (= 3.6 kmh), it's "Guarding", if it's faster, it's -- moving with direction local theUnit = theGroup:getUnit(1) local vvel = theUnit:getVelocity() local vel = dcsCommon.vMag(vvel) if vel < 1 then msg = "apparently guarding" else local speed = "" if vel < 3 then speed = "slowly" elseif vel < 6 then speed = "deliberately" else speed = "briskly" end local heading = dcsCommon.getUnitHeading(theUnit) -- in rad msg = speed .. " moving " .. dcsCommon.bearing2compass(heading) end return msg end function cfxReconMode.getLocation(theGroup) local msg = "" local theUnit = theGroup:getUnit(1) local currPoint = theUnit:getPoint() local ele = math.floor(land.getHeight({x = currPoint.x, y = currPoint.z})) local units = "m" if cfxReconMode.imperialUnits then ele = math.floor(ele * 3.28084) -- feet units = "ft" else ele = math.floor(ele) -- meters end if cfxReconMode.mgrs then local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) msg = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing .. " Ele " .. ele .. units else local lat, lon, alt = coord.LOtoLL(currPoint) lat, lon = dcsCommon.latLon2Text(lat, lon) msg = "Lat " .. lat .. " Lon " .. lon .. " Ele " .. ele ..units end return msg end function cfxReconMode.getTimeData() local msg = "" local absSecs = timer.getAbsTime()-- + env.mission.start_time while absSecs > 86400 do absSecs = absSecs - 86400 -- subtract out all days end msg = dcsCommon.processHMS("<:h>:<:m>:<:s>", absSecs) return "at " .. msg end function cfxReconMode.generateSALT(theScout, theGroup) local msg = theScout:getName() .. " reports new ground contact" if cfxReconMode.groupNames then msg = msg .. " " .. theGroup:getName() end msg = msg .. ":\n" -- SALT: S = Situation or number of units A = action they are doing L = Location T = Time msg = msg .. cfxReconMode.getSit(theGroup) .. ", "-- S msg = msg .. cfxReconMode.getAction(theGroup) .. ", " -- A msg = msg .. cfxReconMode.getLocation(theGroup) .. ", " -- L msg = msg .. cfxReconMode.getTimeData() -- T return msg end function cfxReconMode.processZoneMessage(inMsg, theZone, theGroup) if not inMsg then return "" end local formerType = type(inMsg) if formerType ~= "string" then inMsg = tostring(inMsg) end if not inMsg then inMsg = "" end local outMsg = "" -- replace line feeds outMsg = inMsg:gsub("", "\n") if theZone then outMsg = outMsg:gsub("", theZone.name) end -- replace with current mission time HMS local absSecs = timer.getAbsTime()-- + env.mission.start_time while absSecs > 86400 do absSecs = absSecs - 86400 -- subtract out all days end local timeString = dcsCommon.processHMS("<:h>:<:m>:<:s>", absSecs) outMsg = outMsg:gsub("", timeString) -- replace with lat of zone point and with lon of zone point -- and with mgrs coords of zone point local currPoint = theZone:getPoint() if theGroup and theGroup:isExist() then -- only use group's point when group exists and alive local theUnit = dcsCommon.getFirstLivingUnit(theGroup) currPoint = theUnit:getPoint() end local ele = math.floor(land.getHeight({x = currPoint.x, y = currPoint.z})) local units = "m" if cfxReconMode.imperialUnits then ele = math.floor(ele * 3.28084) -- feet units = "ft" else ele = math.floor(ele) -- meters end local lat, lon, alt = coord.LOtoLL(currPoint) lat, lon = dcsCommon.latLon2Text(lat, lon) outMsg = outMsg:gsub("", lat) outMsg = outMsg:gsub("", lon) outMsg = outMsg:gsub("", ele..units) --currPoint = cfxZones.getPoint(theZone) local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing outMsg = outMsg:gsub("", mgrs) return outMsg end function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) -- see if it was a prio target and gather info local inList, gName = cfxReconMode.isStringInList(theGroup:getName(), cfxReconMode.prioList) local silent = false if gName and cfxReconMode.zoneInfo[gName] then local zInfo = cfxReconMode.zoneInfo[gName] silent = zInfo.silent end -- put a mark on the map if (not silent) and cfxReconMode.applyMarks then local theID = cfxReconMode.placeMarkForUnit(theLoc, mySide, theGroup) local gName = theGroup:getName() local args = {mySide, theScout, theGroup, theID, gName} cfxReconMode.activeMarks[gName] = args -- schedule removal if desired if cfxReconMode.marksFadeAfter > 0 then timer.scheduleFunction(cfxReconMode.removeMarkForArgs, args, timer.getTime() + cfxReconMode.marksFadeAfter) end end -- say something if (not silent) and cfxReconMode.announcer then local msg = cfxReconMode.generateSALT(theScout, theGroup) trigger.action.outTextForCoalition(mySide, msg, cfxReconMode.reportTime) if cfxReconMode.verbose then trigger.action.outText("+++rcn: announced for side " .. mySide, 30) end -- play a sound trigger.action.outSoundForCoalition(mySide, cfxReconMode.reconSound) else end -- see if it was a prio target if inList then if cfxReconMode.verbose then trigger.action.outText("+++rcn: Priority target spotted", 30) end -- invoke callbacks cfxReconMode.invokeCallbacks("priority", mySide, theScout, theGroup, theGroup:getName()) -- increase prio flag if cfxReconMode.prioFlag then cfxReconMode.theZone:pollFlag(cfxReconMode.prioFlag, cfxReconMode.method ) end -- see if we were passed additional info in zInfo if gName and cfxReconMode.zoneInfo[gName] then local zInfo = cfxReconMode.zoneInfo[gName] if zInfo.prioMessage then -- prio message displays even when announcer is off -- AND EVEN WHEN SILENT!!! local msg = zInfo.prioMessage msg = cfxReconMode.processZoneMessage(msg, zInfo.theZone, theGroup) trigger.action.outTextForCoalition(mySide, msg, cfxReconMode.reportTime) if cfxReconMode.verbose or zInfo.theZone.verbose then trigger.action.outText("+++rcn: prio message sent for prio target zone <" .. zInfo.theZone.name .. ">",30) end end if zInfo.theFlag then zInfo.theZone:pollFlag(zInfo.theFlag, cfxReconMode.method) if cfxReconMode.verbose or zInfo.theZone.verbose then trigger.action.outText("+++rcn: banging <" .. zInfo.theFlag .. "> for prio target zone <" .. zInfo.theZone.name .. ">",30) end end end else -- invoke callbacks cfxReconMode.invokeCallbacks("detected", mySide, theScout, theGroup, theGroup:getName()) -- increase normal flag if cfxReconMode.detectFlag then cfxReconMode.theZone:pollFlag(cfxReconMode.detectFlag, cfxReconMode.method) end end end function cfxReconMode.performReconForUnit(theScout) if not theScout then return end if not theScout:isExist() then return end -- will be gc'd soon -- get altitude above ground to calculate visual range local alt = dcsCommon.getUnitAGL(theScout) local visRange = dcsCommon.lerp(cfxReconMode.detectionMinRange, cfxReconMode.detectionMaxRange, alt/cfxReconMode.maxAlt) local scoutPos = theScout:getPoint() -- figure out which groups we are looking for local myCoal = theScout:getCoalition() local enemyCoal = 1 if myCoal == 1 then enemyCoal = 2 end -- iterate all enemy units until we find one -- and then stop this iteration (can only detect one -- group per pass) local enemyGroups = coalition.getGroups(enemyCoal) for idx, theGroup in pairs (enemyGroups) do -- make sure it's a ground unit local isGround = theGroup:getCategory() == 2 if theGroup:isExist() and isGround then local visible, location = cfxReconMode.canDetect(scoutPos, theGroup, visRange) if visible then -- see if we already detected this one local groupName = theGroup:getName() if cfxReconMode.detectedGroups[groupName] == nil then -- only now check against blackList local inList, gName = cfxReconMode.isStringInList(groupName, cfxReconMode.blackList) if not inList then -- visible and not yet seen -- perhaps add some percent chance now -- remember that we know this group cfxReconMode.detectedGroups[groupName] = theGroup cfxReconMode.detectedGroup(myCoal, theScout, theGroup, location) return -- stop, as we only detect one group per pass end end end end end end function cfxReconMode.doActivate() cfxReconMode.active = true if cfxReconMode.verbose then trigger.action.outText("Recon Mode has activated", 30) end end function cfxReconMode.doDeActivate() cfxReconMode.active = false if cfxReconMode.verbose then trigger.action.outText("Recon Mode is OFF", 30) end end function cfxReconMode.updateQueues() -- schedule next call timer.scheduleFunction(cfxReconMode.updateQueues, {}, timer.getTime() + 1/cfxReconMode.ups) -- check to turn on or off -- check the flags for on/off if cfxReconMode.activate then if cfxZones.testZoneFlag(cfxReconMode, cfxReconMode.activate, "change","lastActivate") then cfxReconMode.doActivate() end end if cfxReconMode.deactivate then if cfxZones.testZoneFlag(cfxReconMode, cfxReconMode.deactivate, "change","lastDeActivate") then cfxReconMode.doDeActivate() end end -- check if we are active if not cfxReconMode.active then return end -- we only process the first aircraft in -- the scouts array, move it to processed and then shrink -- scouts table until it's empty. When empty, transfer all -- back and start cycle anew local theFocusScoutName = nil local procCount = 0 -- no iterations done yet for name, scout in pairs(cfxReconMode.scouts) do theFocusScoutName = name -- remember so we can delete if not scout:isExist() then -- we ignore the scout, and it's -- forgotten since no longer transferred -- i.e. built-in GC if cfxReconMode.verbose then trigger.action.outText("+++rcn: GC - removing scout " .. name .. " because it no longer exists", 30) end -- invoke 'end' for this scout cfxReconMode.invokeCallbacks("dead", -1, nil, nil, name) else -- scan for this scout cfxReconMode.performReconForUnit(scout) -- move it to processed table cfxReconMode.processedScouts[name] = scout end procCount = 1 -- remember we went through one iteration break -- always end after first iteration end -- remove processed scouts from scouts array if procCount > 0 then -- we processed one scout (even if scout itself did not exist) -- remove that scout from active scouts table cfxReconMode.scouts[theFocusScoutName] = nil else -- scouts is empty. copy processed table back to scouts -- restart scouts array, contains GC already cfxReconMode.scouts = cfxReconMode.processedScouts cfxReconMode.processedScouts = {} -- start new empty processed queue end end function cfxReconMode.isGroupStillAlive(gName) local theGroup = Group.getByName(gName) if not theGroup then return false end if not theGroup:isExist() then return false end local allUnits = theGroup:getUnits() for idx, aUnit in pairs (allUnits) do if aUnit:getLife() >= 1 then return true end end return false end function cfxReconMode.autoRemove() -- schedule next call timer.scheduleFunction(cfxReconMode.autoRemove, {}, timer.getTime() + 1/cfxReconMode.ups) local toRemove = {} -- scan all marked groups, and when they no longer exist, remove them for idx, args in pairs (cfxReconMode.activeMarks) do local gName = args[5] if not cfxReconMode.isGroupStillAlive(gName) then -- remove mark, remove group from set table.insert(toRemove, args) end end for idx, args in pairs(toRemove) do cfxReconMode.removeMarkForArgs(args) trigger.action.outText("+++recn: removed mark: " .. args[5], 30) end end -- late eval player function cfxReconMode.lateEvalPlayerUnit(theUnit) -- check if a player is inside one of the scout zones -- first: quick check if the player is already in a list local aGroup = theUnit:getGroup() local gName = aGroup:getName() if cfxReconMode.allowedScouts[gName] then return end if cfxReconMode.blindScouts[gName] then return end -- get location local p = theUnit:getPoint() -- iterate all scoutZones for idx, theZone in pairs (cfxReconMode.scoutZones) do local isScout = theZone.isScout local dynamic = theZone.dynamic local inZone = theZone:pointInZone(p) if inZone then if isScout then cfxReconMode.addToAllowedScoutList(aGroup, dynamic) if cfxReconMode.verbose or theZone.verbose then if dynamic then trigger.action.outText("+++rcn: added LATE DYNAMIC PLAYER" .. gName .. " to allowed scouts", 30) else trigger.action.outText("+++rcn: added LATE PLAYER " .. gName .. " to allowed scouts", 30) end end else cfxReconMode.addToBlindScoutList(aGroup, dynamic) if cfxReconMode.verbose or theZone.verbose then if dynamic then trigger.action.outText("+++rcn: added LATE DYNAMIC PLAYER" .. gName .. " to BLIND scouts list", 30) else trigger.action.outText("+++rcn: added LATE PLAYER " .. gName .. " to BLIND scouts list", 30) end end end return -- we stop after first found end end end -- event handler function cfxReconMode:onEvent(event) if not event then return end if not event.initiator then return end if not (event.id == 15 or event.id == 3) then return end local theUnit = event.initiator if not theUnit:isExist() then return end if not theUnit.getGroup then -- strange, but seemingly can happen return end local theGroup = theUnit:getGroup() if not theGroup then return end local gCat = theGroup:getCategory() -- only continue if cat = 0 (aircraft) or 1 (helo) if gCat > 1 then return end -- we simply add scouts as they are garbage-collected -- every so often when they do not exist if event.id == 15 or -- birth event.id == 3 -- take-off. should already have been taken -- care of by birth, but you never know then -- check if a side must not have scouts. -- this will prevent player units to auto- -- scout when they are on that side. in that case -- you must add manually local theSide = theUnit:getCoalition() local isPlayer = theUnit:getPlayerName() if isPlayer then -- since players wake up late, we lazy-eval their group -- and add it to the blind/scout lists cfxReconMode.lateEvalPlayerUnit(theUnit) if cfxReconMode.verbose then trigger.action.outText("+++rcn: late player check complete for <" .. theUnit:getName() .. ">", 30) end else isPlayer = false -- safer than sorry end if cfxReconMode.autoRecon then if theSide == 0 and not cfxReconMode.greyScouts then return -- grey scouts are not allowed end if theSide == 1 and not cfxReconMode.redScouts then return -- grey scouts are not allowed end if theSide == 2 and not cfxReconMode.blueScouts then return -- grey scouts are not allowed end if cfxReconMode.playerOnlyRecon then if not isPlayer then if cfxReconMode.verbose then trigger.action.outText("+++rcn: <" .. theUnit:getName() .. "> filtered: no player unit", 30) end return -- only players can do recon. this unit is AI end end end -- check if cfxReconMode.autoRecon is enabled -- otherwise, abort the aircraft is not in -- scourlist local gName = theGroup:getName() if not cfxReconMode.autoRecon then -- no auto-recon. plane must be in scouts list local inList, ignored = cfxReconMode.isStringInList(gName, cfxReconMode.allowedScouts) if not inList then if cfxReconMode.verbose then trigger.action.outText("+++rcn: <" .. theUnit:getName() .. "> filtered: not in scout list", 30) end return end end -- check if aircraft is in blindlist -- abort if so local inList, ignored = cfxReconMode.isStringInList(gName, cfxReconMode.blindScouts) if inList then if cfxReconMode.verbose then trigger.action.outText("+++rcn: <" .. theUnit:getName() .. "> filtered: unit cannot scout", 30) end return end if cfxReconMode.verbose then trigger.action.outText("+++rcn: event " .. event.id .. " for unit " .. theUnit:getName(), 30) end cfxReconMode.addScout(theUnit) end end -- -- read all existing planes -- function cfxReconMode.processScoutGroups(theGroups) for idx, aGroup in pairs(theGroups) do -- process all planes in that group -- we are very early in the mission, only few groups really -- exist now, the rest of the units come in with 15 event if aGroup:isExist() then -- see if we want to add these aircraft to the -- active scout list local gName = aGroup:getName() local isBlind, ignored = cfxReconMode.isStringInList(gName, cfxReconMode.blindScouts) local isScout, ignored = cfxReconMode.isStringInList(gName, cfxReconMode.allowedScouts) local doAdd = cfxReconMode.autoRecon if cfxReconMode.autoRecon then local theSide = aGroup:getCoalition() if theSide == 0 and not cfxReconMode.greyScouts then doAdd = false elseif theSide == 1 and not cfxReconMode.redScouts then doAdd = false elseif theSide == 2 and not cfxReconMode.blueScouts then doAdd = false end end if isBlind then doAdd = false end if isScout then doAdd = true end -- overrides all if doAdd then local allUnits = Group.getUnits(aGroup) for idy, aUnit in pairs (allUnits) do if aUnit:isExist() then if cfxReconMode.autoRecon and cfxReconMode.playerOnlyRecon and (aUnit:getPlayerName() == nil) then if cfxReconMode.verbose then trigger.action.outText("+++rcn: skipped unit " ..aUnit:getName() .. " because not player unit", 30) end else cfxReconMode.addScout(aUnit) if cfxReconMode.verbose then trigger.action.outText("+++rcn: added unit " ..aUnit:getName() .. " to pool at startup", 30) end end end end else if cfxReconMode.verbose then trigger.action.outText("+++rcn: filtered group " .. gName .. " from being entered into scout pool at startup", 30) end end end end end function cfxReconMode.initScouts() -- get all groups of aircraft. Unrolled loop 0..2 -- added helicopters, removed check for grey/red/bluescouts, -- as that happens in processScoutGroups local theAirGroups = {} theAirGroups = coalition.getGroups(0, 0) -- 0 = aircraft cfxReconMode.processScoutGroups(theAirGroups) theAirGroups = coalition.getGroups(0, 1) -- 1 = helicopter cfxReconMode.processScoutGroups(theAirGroups) theAirGroups = coalition.getGroups(1, 0) -- 0 = aircraft cfxReconMode.processScoutGroups(theAirGroups) theAirGroups = coalition.getGroups(1, 1) -- 1 = helicopter cfxReconMode.processScoutGroups(theAirGroups) theAirGroups = coalition.getGroups(2, 0) -- 0 = aircraft cfxReconMode.processScoutGroups(theAirGroups) theAirGroups = coalition.getGroups(2, 1) -- 1 = helicopter cfxReconMode.processScoutGroups(theAirGroups) end -- -- read config -- function cfxReconMode.readConfigZone() -- note: must match exactly!!!! local theZone = cfxZones.getZoneByName("reconModeConfig") if not theZone then if cfxReconMode.verbose then trigger.action.outText("+++rcn: no config zone!", 30) end theZone = cfxZones.createSimpleZone("reconModeConfig") else if cfxReconMode.verbose then trigger.action.outText("+++rcn: found config zone!", 30) end end cfxReconMode.verbose = theZone.verbose --cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) cfxReconMode.autoRecon = theZone:getBoolFromZoneProperty("autoRecon", true) cfxReconMode.redScouts = theZone:getBoolFromZoneProperty("redScouts", false) cfxReconMode.blueScouts = theZone:getBoolFromZoneProperty( "blueScouts", true) cfxReconMode.greyScouts = theZone:getBoolFromZoneProperty( "greyScouts", false) cfxReconMode.playerOnlyRecon = theZone:getBoolFromZoneProperty("playerOnlyRecon", false) cfxReconMode.reportNumbers = theZone:getBoolFromZoneProperty( "reportNumbers", true) cfxReconMode.reportTime = theZone:getNumberFromZoneProperty( "reportTime", 30) cfxReconMode.detectionMinRange = theZone:getNumberFromZoneProperty("detectionMinRange", 3000) cfxReconMode.detectionMaxRange = theZone:getNumberFromZoneProperty("detectionMaxRange", 12000) cfxReconMode.maxAlt = theZone:getNumberFromZoneProperty("maxAlt", 9000) if theZone:hasProperty("prio+") then -- deprecated. remove next update cfxReconMode.prioFlag = theZone:getStringFromZoneProperty("prio+", "none") elseif theZone:hasProperty("prio!") then cfxReconMode.prioFlag = theZone:getStringFromZoneProperty("prio!", "*") end if theZone:hasProperty("detect+") then -- deprecated cfxReconMode.detectFlag = theZone:getStringFromZoneProperty("detect+", "none") elseif theZone:hasProperty("detect!") then cfxReconMode.detectFlag = theZone:getStringFromZoneProperty("detect!", "*") end cfxReconMode.method = theZone:getStringFromZoneProperty("method", "inc") if theZone:hasProperty("reconMethod") then cfxReconMode.method = theZone:getStringFromZoneProperty("reconMethod", "inc") end cfxReconMode.applyMarks = theZone:getBoolFromZoneProperty( "applyMarks", true) cfxReconMode.marksFadeAfter = theZone:getNumberFromZoneProperty("marksFadeAfter", 30*60) -- 30 minutes default cfxReconMode.marksLocked = theZone:getBoolFromZoneProperty("marksLocked", false) -- if true, players cannot remove the marks cfxReconMode.announcer = theZone:getBoolFromZoneProperty( "announcer", true) if theZone:hasProperty("reconSound") then cfxReconMode.reconSound = theZone:getStringFromZoneProperty("reconSound", "") end cfxReconMode.removeWhenDestroyed = theZone:getBoolFromZoneProperty("autoRemove", true) cfxReconMode.mgrs = theZone:getBoolFromZoneProperty("mgrs", false) cfxReconMode.active = theZone:getBoolFromZoneProperty("active", true) if theZone:hasProperty("activate?") then cfxReconMode.activate = theZone:getStringFromZoneProperty("activate?", "*") cfxReconMode.lastActivate = theZone:getFlagValue(cfxReconMode.activate) elseif theZone:hasProperty("on?") then cfxReconMode.activate = theZone:getStringFromZoneProperty("on?", "*") cfxReconMode.lastActivate = theZone:getFlagValue(cfxReconMode.activate) end if theZone:hasProperty("deactivate?") then cfxReconMode.deactivate = theZone:getStringFromZoneProperty("deactivate?", "*") cfxReconMode.lastDeActivate = theZone:getFlagValue(cfxReconMode.deactivate) elseif theZone:hasProperty("off?") then cfxReconMode.deactivate = theZone:getStringFromZoneProperty("off?", "*") cfxReconMode.lastDeActivate = theZone:getFlagValue(cfxReconMode.deactivate) end cfxReconMode.imperialUnits = theZone:getBoolFromZoneProperty("imperial", false) if theZone:hasProperty("imperialUnits") then cfxReconMode.imperialUnits = theZone:getBoolFromZoneProperty( "imperialUnits", false) end cfxReconMode.groupNames = theZone:getBoolFromZoneProperty( "groupNames", true) cfxReconMode.theZone = theZone -- save this zone end -- -- read blackList and prio list groups -- function cfxReconMode.processReconZone(theZone) local theList = theZone:getStringFromZoneProperty("recon", "prio") theList = string.upper(theList) local isBlack = dcsCommon.stringStartsWith(theList, "BLACK") local zInfo = {} zInfo.theZone = theZone zInfo.isBlack = isBlack zInfo.silent = theZone:getBoolFromZoneProperty("silent", false) if theZone:hasProperty("spotted!") then zInfo.theFlag = theZone:getStringFromZoneProperty("spotted!", "*") end if theZone:hasProperty("prioMessage") then zInfo.prioMessage = theZone:getStringFromZoneProperty("prioMessage", "") end local dynamic = theZone:getBoolFromZoneProperty("dynamic", false) zInfo.dynamic = dynamic local categ = 2 -- ground troops only local allGroups = cfxZones.allGroupsInZone(theZone, categ) for idx, aGroup in pairs(allGroups) do local gName = aGroup:getName() cfxReconMode.zoneInfo[gName] = zInfo if isBlack then cfxReconMode.addToBlackList(aGroup, dynamic) if cfxReconMode.verbose or theZone.verbose then if dynamic then trigger.action.outText("+++rcn: added DYNAMIC " .. aGroup:getName() .. " to blacklist", 30) else trigger.action.outText("+++rcn: added " .. aGroup:getName() .. " to blacklist", 30) end end else cfxReconMode.addToPrioList(aGroup, dynamic) if cfxReconMode.verbose or theZone.verbose then if dynamic then trigger.action.outText("+++rcn: added DYNAMIC " .. aGroup:getName() .. " to priority target list", 30) else trigger.action.outText("+++rcn: added " .. aGroup:getName() .. " to priority target list", 30) end end end end end function cfxReconMode.processScoutZone(theZone) local isScout = theZone:getBoolFromZoneProperty("scout", true) local dynamic = theZone:getBoolFromZoneProperty("dynamic") theZone.dynamic = dynamic theZone.isScout = isScout local categ = 0 -- aircraft local allFixed = theZone:allGroupsInZone(categ) local categ = 1 -- helos local allRotor = theZone:allGroupsInZone(categ) local allGroups = dcsCommon.combineTables(allFixed, allRotor) for idx, aGroup in pairs(allGroups) do if isScout then cfxReconMode.addToAllowedScoutList(aGroup, dynamic) if cfxReconMode.verbose or theZone.verbose then if dynamic then trigger.action.outText("+++rcn: added DYNAMIC " .. aGroup:getName() .. " to allowed scouts", 30) else trigger.action.outText("+++rcn: added " .. aGroup:getName() .. " to allowed scouts", 30) end end else cfxReconMode.addToBlindScoutList(aGroup, dynamic) if cfxReconMode.verbose or theZone.verbose then if dynamic then trigger.action.outText("+++rcn: added DYNAMIC " .. aGroup:getName() .. " to BLIND scouts list", 30) else trigger.action.outText("+++rcn: added " .. aGroup:getName() .. " to BLIND scouts list", 30) end end end end table.insert(cfxReconMode.scoutZones, theZone) end function cfxReconMode.readReconGroups() local attrZones = cfxZones.getZonesWithAttributeNamed("recon") for k, aZone in pairs(attrZones) do cfxReconMode.processReconZone(aZone) end end function cfxReconMode.readScoutGroups() local attrZones = cfxZones.getZonesWithAttributeNamed("scout") for k, aZone in pairs(attrZones) do cfxReconMode.processScoutZone(aZone) end end -- -- start -- function cfxReconMode.start() -- lib check if not dcsCommon.libCheck("cfx Recon Mode", cfxReconMode.requiredLibs) then return false end -- read config cfxReconMode.readConfigZone() -- gather prio and blacklist groups cfxReconMode.readReconGroups() -- gather allowed and forbidden scouts cfxReconMode.readScoutGroups() -- gather exiting planes cfxReconMode.initScouts() -- start update cycle cfxReconMode.updateQueues() -- if dead groups are removed from map, -- schedule housekeeping if cfxReconMode.removeWhenDestroyed then cfxReconMode.autoRemove() end if true or cfxReconMode.autoRecon then -- install own event handler to detect -- when a unit takes off and add it to scout -- roster world.addEventHandler(cfxReconMode) end trigger.action.outText("cfx Recon version " .. cfxReconMode.version .. " started.", 30) return true end -- -- test callback -- function cfxReconMode.demoReconCB(reason, theSide, theScout, theGroup, theName) trigger.action.outText("recon CB: " .. reason .. " -- " .. theScout:getName() .. " spotted " .. theName, 30) end if not cfxReconMode.start() then cfxReconMode = nil end --[[-- ideas: - renew lease. when already sighted, simply renew lease, maybe update location. - update marks and renew lease TODO: red+ and blue+ - flags to increase when a plane of the other side is detected --]]--