diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index bb0b1d8..3c818c7 100644 Binary files a/Doc/DML Documentation.pdf and b/Doc/DML Documentation.pdf differ diff --git a/Doc/DML Quick Reference.pdf b/Doc/DML Quick Reference.pdf index cdbf059..789e345 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/cfxArtilleryZones.lua b/modules/cfxArtilleryZones.lua index 4526902..c8ecb89 100644 --- a/modules/cfxArtilleryZones.lua +++ b/modules/cfxArtilleryZones.lua @@ -1,5 +1,5 @@ cfxArtilleryZones = {} -cfxArtilleryZones.version = "2.2.0" +cfxArtilleryZones.version = "2.2.1" cfxArtilleryZones.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -29,6 +29,7 @@ cfxArtilleryZones.verbose = false 2.1.0 - DML Flag Support - code cleanup 2.2.0 - DML Watchflag integration + 2.2.1 - minor code clean-up Artillery Target Zones *** EXTENDS ZONES *** Target Zones for artillery. Can determine which zones are in range and visible and then handle artillery barrage to this zone @@ -136,11 +137,7 @@ function cfxArtilleryZones.processArtilleryZone(aZone) if cfxZones.hasProperty(aZone, "f?") then aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty(aZone, "f?", "none") end - --[[-- - if cfxZones.hasProperty(aZone, "triggerFlag") then - aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty(aZone, "triggerFlag", "none") - end - --]]-- + if cfxZones.hasProperty(aZone, "artillery?") then aZone.artyTriggerFlag = cfxZones.getStringFromZoneProperty(aZone, "artillery?", "none") end diff --git a/modules/cfxGroudTroops.lua b/modules/cfxGroundTroops.lua similarity index 100% rename from modules/cfxGroudTroops.lua rename to modules/cfxGroundTroops.lua diff --git a/modules/cfxReconMode.lua b/modules/cfxReconMode.lua index ffb51d7..982391a 100644 --- a/modules/cfxReconMode.lua +++ b/modules/cfxReconMode.lua @@ -1,11 +1,16 @@ cfxReconMode = {} -cfxReconMode.version = "1.5.0" +cfxReconMode.version = "2.0.0" 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 @@ -48,6 +53,26 @@ VERSION HISTORY 1.5.0 - removeWhenDestroyed() - autoRemove() - readConfigZone creates default config zone so we get correct defaulting + 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 cfxReconMode is a script that allows units to perform reconnaissance missions and, after detecting units, marks them on the map with @@ -79,7 +104,9 @@ 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. @@ -111,38 +138,80 @@ function cfxReconMode.invokeCallbacks(reason, theSide, theScout, theGroup, theNa end -- add a priority/blackList group name to prio list -function cfxReconMode.addToPrioList(aGroup) +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 - table.insert(cfxReconMode.prioList, aGroup) +-- table.insert(cfxReconMode.prioList, aGroup) + cfxReconMode.prioList[aGroup] = aGroup + cfxReconMode.dynamics[aGroup] = dynamic end end -function cfxReconMode.addToBlackList(aGroup) +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 - table.insert(cfxReconMode.blackList, aGroup) + --table.insert(cfxReconMode.blackList, aGroup) + 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) - if not theString then return false end - if not theList then return false end - if type(theString) == "string" then - for idx,anItem in pairs(theList) do - if anItem == theString then return true end + -- 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 + + return false, nil end + -- addScout directly adds a scout unit. Use from external -- to manually add a unit (e.g. via GUI when autoscout isExist -- off, or to force a scout unit (e.g. when scouts for a side @@ -205,7 +274,7 @@ end function cfxReconMode.removeScout(theUnit) if not theUnit then - trigger.action.outText("+++cfxRecon: WARNING - nil Unit on remove", 30) + trigger.action.outText("+++rcn: WARNING - nil Unit on remove", 30) return end @@ -265,7 +334,8 @@ function cfxReconMode.placeMarkForUnit(location, theSide, theGroup) local theID = cfxReconMode.uuid() local theDesc = "Contact: "..theGroup:getName() if cfxReconMode.reportNumbers then - theDesc = theDesc .. " (" .. theGroup:getSize() .. " units)" +-- theDesc = theDesc .. " (" .. theGroup:getSize() .. " units)" + theDesc = theDesc .. " - " .. cfxReconMode.getSit(theGroup) .. ", " .. cfxReconMode.getAction(theGroup) .. "." end trigger.action.markToCoalition( theID, @@ -296,6 +366,121 @@ function cfxReconMode.removeMarkForArgs(args) 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() + if cfxReconMode.mgrs then + local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) + msg = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing + else + local lat, lon, alt = coord.LOtoLL(currPoint) + lat, lon = dcsCommon.latLon2Text(lat, lon) + msg = "Lat " .. lat .. " Lon " .. lon + 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 " .. theGroup:getName() .. ":\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) + 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 = cfxZones.getPoint(theZone) + local lat, lon, alt = coord.LOtoLL(currPoint) + lat, lon = dcsCommon.latLon2Text(lat, lon) + outMsg = outMsg:gsub("", lat) + outMsg = outMsg:gsub("", lon) + 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) -- put a mark on the map @@ -312,26 +497,51 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) -- say something if cfxReconMode.announcer then - trigger.action.outTextForCoalition(mySide, theScout:getName() .. " reports new ground contact " .. theGroup:getName(), 30) - trigger.action.outText("+++recon: announced for side " .. mySide, 30) + local msg = cfxReconMode.generateSALT(theScout, theGroup) + trigger.action.outTextForCoalition(mySide, msg, 30) +-- trigger.action.outTextForCoalition(mySide, theScout:getName() .. " reports new ground contact " .. theGroup:getName(), 30) + if cfxReconMode.verbose then + trigger.action.outText("+++rcn: announced for side " .. mySide, 30) + end -- play a sound trigger.action.outSoundForCoalition(mySide, cfxReconMode.reconSound) else - --trigger.action.outText("+++recon: announcer off", 30) end -- see if it was a prio target - if cfxReconMode.isStringInList(theGroup:getName(), cfxReconMode.prioList) then - if cfxReconMode.announcer then - trigger.action.outTextForCoalition(mySide, "Priority target confirmed", 30) + local inList, gName = cfxReconMode.isStringInList(theGroup:getName(), cfxReconMode.prioList) + if inList then +-- if cfxReconMode.announcer then + if cfxReconMode.verbose then + trigger.action.outText("+++rcn: Priority target spotted", 30) end -- invoke callbacks - cfxReconMode.invokeCallbacks("priotity", mySide, theScout, theGroup, theGroup:getName()) + cfxReconMode.invokeCallbacks("priority", mySide, theScout, theGroup, theGroup:getName()) -- increase prio flag if cfxReconMode.prioFlag then - local currVal = trigger.misc.getUserFlag(cfxReconMode.prioFlag) - trigger.action.setUserFlag(cfxReconMode.prioFlag, currVal + 1) + cfxZones.pollFlag(cfxReconMode.prioFlag, cfxReconMode.method, cfxReconMode.theZone) + 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 + local msg = zInfo.prioMessage + msg = cfxReconMode.processZoneMessage(msg, zInfo.theZone) + trigger.action.outTextForCoalition(mySide, msg, 30) + 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 + cfxZones.pollFlag(zInfo.theFlag, cfxReconMode.method, zInfo.theZone) + 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 @@ -339,8 +549,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) -- increase normal flag if cfxReconMode.detectFlag then - local currVal = trigger.misc.getUserFlag(cfxReconMode.detectFlag) - trigger.action.setUserFlag(cfxReconMode.detectFlag, currVal + 1) + cfxZones.pollFlag(cfxReconMode.detectFlag, cfxReconMode.method, cfxReconMode.theZone) end end end @@ -371,7 +580,8 @@ function cfxReconMode.performReconForUnit(theScout) local groupName = theGroup:getName() if cfxReconMode.detectedGroups[groupName] == nil then -- only now check against blackList - if not cfxReconMode.isStringInList(groupName, cfxReconMode.blackList) then + 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 @@ -464,11 +674,62 @@ function cfxReconMode.autoRemove() 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 = cfxZones.pointInZone(p, theZone) + 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 + local theGroup = theUnit:getGroup() +-- trigger.action.outText("+++rcn-ENTER onEvent: " .. event.id .. " for <" .. theUnit:getName() .. ">", 30) + 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 @@ -481,26 +742,71 @@ function cfxReconMode:onEvent(event) -- scout when they are on that side. in that case -- you must add manually local theSide = theUnit:getCoalition() - 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 theUnit:getPlayerName() then - return -- only players can do recon. this unit is AI + 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 +-- trigger.action.outText("+++rcn-onEvent: " .. event.id .. " for <" .. theUnit:getName() .. ">", 30) end -- @@ -512,35 +818,73 @@ function cfxReconMode.processScoutGroups(theGroups) -- 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 - local allUnits = Group.getUnits(aGroup) - for idy, aUnit in pairs (allUnits) do - if aUnit:isExist() then - cfxReconMode.addScout(aUnit) - if cfxReconMode.verbose then - trigger.action.outText("+++rcn: added unit " ..aUnit:getName() .. " to pool at startup", 30) - end - end + -- 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 = {} - if cfxReconMode.greyScouts then - theAirGroups = coalition.getGroups(0, 0) -- 0 = aircraft - cfxReconMode.processScoutGroups(theAirGroups) - end - if cfxReconMode.redScouts then - theAirGroups = coalition.getGroups(1, 0) -- 1 = red, 0 = aircraft - cfxReconMode.processScoutGroups(theAirGroups) - end - - if cfxReconMode.blueScouts then - theAirGroups = coalition.getGroups(2, 0) -- 2 = blue, 0 = aircraft - cfxReconMode.processScoutGroups(theAirGroups) - end + 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 -- @@ -575,12 +919,20 @@ function cfxReconMode.readConfigZone() if cfxZones.hasProperty(theZone, "prio+") then cfxReconMode.prioFlag = cfxZones.getStringFromZoneProperty(theZone, "prio+", "none") + elseif cfxZones.hasProperty(theZone, "prio!") then + cfxReconMode.prioFlag = cfxZones.getStringFromZoneProperty(theZone, "prio!", "*") end if cfxZones.hasProperty(theZone, "detect+") then cfxReconMode.detectFlag = cfxZones.getStringFromZoneProperty(theZone, "detect+", "none") + elseif cfxZones.hasProperty(theZone, "detect!") then + cfxReconMode.detectFlag = cfxZones.getStringFromZoneProperty(theZone, "detect!", "*") end + cfxReconMode.method = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") + if cfxZones.hasProperty(theZone, "reconMethod") then + cfxReconMode.method = cfxZones.getStringFromZoneProperty(theZone, "reconMethod", "inc") + end cfxReconMode.applyMarks = cfxZones.getBoolFromZoneProperty(theZone, "applyMarks", true) cfxReconMode.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true) @@ -591,6 +943,101 @@ function cfxReconMode.readConfigZone() cfxReconMode.removeWhenDestroyed = cfxZones.getBoolFromZoneProperty(theZone, "autoRemove", true) + cfxReconMode.mgrs = cfxZones.getBoolFromZoneProperty(theZone, "mgrs", false) + + cfxReconMode.theZone = theZone -- save this zone +end + +-- +-- read blackList and prio list groups +-- + + +function cfxReconMode.processReconZone(theZone) + local theList = cfxZones.getStringFromZoneProperty(theZone, "recon", "prio") + theList = string.upper(theList) + local isBlack = dcsCommon.stringStartsWith(theList, "BLACK") + + local zInfo = {} + zInfo.theZone = theZone + zInfo.isBlack = isBlack + if cfxZones.hasProperty(theZone, "spotted!") then + zInfo.theFlag = cfxZones.getStringFromZoneProperty(theZone, "spotted!", "*") + end + + if cfxZones.hasProperty(theZone, "prioMessage") then + zInfo.prioMessage = cfxZones.getStringFromZoneProperty(theZone, "prioMessage", "") + end + + local dynamic = cfxZones.getBoolFromZoneProperty(theZone, "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 = cfxZones.getBoolFromZoneProperty(theZone, "scout", true) + local dynamic = cfxZones.getBoolFromZoneProperty(theZone, "dynamic") + theZone.dynamic = dynamic + theZone.isScout = isScout + + local categ = 0 -- aircraft + local allFixed = cfxZones.allGroupsInZone(theZone, categ) + local categ = 1 -- helos + local allRotor = cfxZones.allGroupsInZone(theZone, 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 -- @@ -606,6 +1053,12 @@ function cfxReconMode.start() -- read config cfxReconMode.readConfigZone() + -- gather prio and blacklist groups + cfxReconMode.readReconGroups() + + -- gather allowed and forbidden scouts + cfxReconMode.readScoutGroups() + -- gather exiting planes cfxReconMode.initScouts() @@ -618,7 +1071,7 @@ function cfxReconMode.start() cfxReconMode.autoRemove() end - if cfxReconMode.autoRecon then + if true or cfxReconMode.autoRecon then -- install own event handler to detect -- when a unit takes off and add it to scout -- roster @@ -651,8 +1104,9 @@ 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 - +TODO: recon: scout and blind for aircraft in group to add / remove scouts, maybe use scout keyword +allow special bangs per priority group --]]-- diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 326bee7..fcaa3ea 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1516,7 +1516,7 @@ function cfxZones.testZoneFlag(theZone, theFlagName, theMethod, latchName) -- get last value from latch local lastVal = theZone[latchName] if not lastVal then - trigger.action.outText("+++Zne: latch <" .. latchName .. "> not valid for zone " .. theZone.name, 30) + trigger.action.outText("+++Zne: latch <" .. latchName .. "> not valid for zone " .. theZone.name, 30) -- intentional break here return nil, nil end diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 05e70f8..83d2fea 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.6.4" +dcsCommon.version = "2.6.5" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -74,6 +74,9 @@ dcsCommon.version = "2.6.4" 2.6.2 - new combineTables() 2.6.3 - new tacan2freq() 2.6.4 - new processHMS() + 2.6.5 - new bearing2compass() + - new bearingdegrees2compass() + - new latLon2Text() - based on mist --]]-- @@ -530,6 +533,25 @@ dcsCommon.version = "2.6.4" return "North" end + function dcsCommon.bearing2compass(inrad) + local bearing = math.floor(inrad / math.pi * 180) + if bearing < 0 then bearing = bearing + 360 end + if bearing > 360 then bearing = bearing - 360 end + return dcsCommon.bearingdegrees2compass(bearing) + end + + function dcsCommon.bearingdegrees2compass(bearing) + if bearing < 23 then return "North" end + if bearing < 68 then return "NE" end + if bearing < 112 then return "East" end + if bearing < 158 then return "SE" end + if bearing < 202 then return "South" end + if bearing < 248 then return "SW" end + if bearing < 292 then return "West" end + if bearing < 338 then return "NW" end + return "North" + end + function dcsCommon.clockPositionOfARelativeToB(A, B, headingOfBInDegrees) -- o'clock notation if not A then return "***error:A***" end @@ -2212,7 +2234,7 @@ function dcsCommon.getUnitHeading(theUnit) local pos = theUnit:getPosition() -- returns three vectors, p is location local heading = math.atan2(pos.x.z, pos.x.x) - -- make sure positive only, add 260 degrees + -- make sure positive only, add 360 degrees if heading < 0 then heading = heading + 2 * math.pi -- put heading in range of 0 to 2*pi end @@ -2255,6 +2277,50 @@ function dcsCommon.coalition2county(inCoalition) end +function dcsCommon.latLon2Text(lat, lon) + -- inspired by mist, thanks Grimes! + -- returns two strings: lat and lon + + -- determine hemispheres by sign + local latHemi, lonHemi + if lat > 0 then latHemi = 'N' else latHemi = 'S' end + if lon > 0 then lonHemi = 'E' else lonHemi = 'W' end + + -- remove sign since we have hemi + lat = math.abs(lat) + lon = math.abs(lon) + + -- calc deg / mins + local latDeg = math.floor(lat) + local latMin = (lat - latDeg) * 60 + local lonDeg = math.floor(lon) + local lonMin = (lon - lonDeg) * 60 + + -- calc seconds + local rawLatMin = latMin + latMin = math.floor(latMin) + local latSec = (rawLatMin - latMin) * 60 + local rawLonMin = lonMin + lonMin = math.floor(lonMin) + local lonSec = (rawLonMin - lonMin) * 60 + + -- correct for rounding errors + if latSec >= 60 then + latSec = latSec - 60 + latMin = latMin + 1 + end + if lonSec >= 60 then + lonSec = lonSec - 60 + lonMin = lonMin + 1 + end + + -- prepare string output + local secFrmtStr = '%06.3f' + local lat = string.format('%02d', latDeg) .. '°' .. string.format('%02d', latMin) .. "'" .. string.format(secFrmtStr, latSec) .. '"' .. latHemi + local lon = string.format('%02d', lonDeg) .. '°' .. string.format('%02d', lonMin) .. "'" .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi + return lat, lon +end + -- -- -- INIT diff --git a/modules/guardianAngel.lua b/modules/guardianAngel.lua index 623fc81..201a7e6 100644 --- a/modules/guardianAngel.lua +++ b/modules/guardianAngel.lua @@ -1,5 +1,5 @@ guardianAngel = {} -guardianAngel.version = "2.0.3" +guardianAngel.version = "3.0.0" guardianAngel.ups = 10 guardianAngel.launchWarning = true -- detect launches and warn pilot guardianAngel.intervention = true -- remove missiles just before hitting @@ -9,6 +9,10 @@ guardianAngel.announcer = true -- angel talks to you guardianAngel.private = false -- angel only talks to group guardianAngel.autoAddPlayers = true +guardianAngel.active = true -- can be turned on / off + +guardianAngel.angelicZones = {} + guardianAngel.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -37,6 +41,18 @@ guardianAngel.requiredLibs = { - can be dangerous 2.0.3 - fxDistance - mea cupa capability + 3.0.0 - on/off and switch monitoring + - active flag + - zones to designate protected aircraft + - zones to designate unprotected aircraft + - improved gA logging + - missilesAndTargets log + - re-targeting detection + - removed bubble check + - retarget Item code + - hardened missile disappear code + - all missiles are now tracked regardless whom they aim for + - removed item.wp This script detects missiles launched against protected aircraft an @@ -51,7 +67,7 @@ guardianAngel.safetyFactor = 1.8 -- for calculating dealloc range guardianAngel.unitsToWatchOver = {} -- I'll watch over these guardianAngel.missilesInTheAir = {} -- missiles in the air - +guardianAngel.missilesAndTargets = {} -- permanent log which missile was aimed at whom guardianAngel.callBacks = {} -- callbacks -- callback signature: callBack(reason, targetUnitName, weaponName) -- reasons (string): "launch", "miss", "reacquire", "trackloss", "disappear", "intervention" @@ -77,9 +93,14 @@ function guardianAngel.addUnitToWatch(aUnit) end if not aUnit then return end local unitName = aUnit:getName() + local isNew = guardianAngel.unitsToWatchOver[unitName] == nil guardianAngel.unitsToWatchOver[unitName] = aUnit if guardianAngel.verbose then - trigger.action.outText("+++gA: now watching unit " .. aUnit:getName(), 30) + if isNew then + trigger.action.outText("+++gA: now watching unit " .. unitName, 30) + else + trigger.action.outText("+++gA: updating unit " .. unitName, 30) + end end end @@ -104,14 +125,17 @@ end -- -- watch q items -- -function guardianAngel.createQItem(theWeapon, theTarget, detectProbability) +function guardianAngel.createQItem(theWeapon, theTarget, threat) if not theWeapon then return nil end if not theTarget then return nil end if not theTarget:isExist() then return nil end - if not detectProbability then detectProbability = 1.0 end + if not threat then threat = false end + -- if an item is not a 'threat' it means that we merely + -- watch it for re-targeting purposes + local theItem = {} - theItem.theWeapon = theWeapon - theItem.wP = theWeapon:getPoint() -- save location + theItem.theWeapon = theWeapon -- weapon that we are tracking + --theItem.wP = theWeapon:getPoint() -- save location theItem.weaponName = theWeapon:getName() theItem.theTarget = theTarget theItem.tGroup = theTarget:getGroup() @@ -119,14 +143,63 @@ function guardianAngel.createQItem(theWeapon, theTarget, detectProbability) theItem.targetName = theTarget:getName() theItem.launchTimeStamp = timer.getTime() - theItem.lastCheckTimeStamp = -1000 + --theItem.lastCheckTimeStamp = -1000 theItem.lastDistance = math.huge theItem.detected = false - theItem.lostTrack = false -- so we can detect sneakies! + --theItem.lostTrack = false -- so we can detect sneakies! theItem.missed = false -- just keep watching for re-ack + theItem.threat = threat + theItem.lastDesc = "(new)" + theItem.timeStamp = timer.getTime() return theItem end +function guardianAngel.retargetItem(theItem, theTarget, threat) + theItem.theTarget = nil -- may cause trouble + if not theTarget or not theTarget:isExist() then + theItem.threat = false + theItem.timeStamp = timer.getTime() + theItem.target = nil + theItem.targetName = "(substitute)" + theItem.lastDistance = math.huge + -- theItem.lostTrack = false + theItem.missed = false + theItem.lastDesc = "(retarget)" + return + end + if not threat then threat = false end + theItem.timeStamp = timer.getTime() + theItem.threat = threat + + theItem.theTarget = theTarget + if not theTarget.getGroup then + local theCat = theTarget:getCategory() + if theCat ~= 2 then + -- not a weapon / flare + trigger.action.outText("*** gA: WARNING: <" .. theTarget:getName() .. "> has no getGroup and is of category <" .. theCat .. ">!!!", 30) + + else + -- target is a weapon (flare/chaff/decoy), all is well + end + else + theItem.tGroup = theTarget:getGroup() + theItem.tID = theItem.tGroup:getID() + end + theItem.targetName = theTarget:getName() + theItem.lastDistance = math.huge + --theItem.lostTrack = false + theItem.missed = false + theItem.lastDesc = "(retarget)" +end + +function guardianAngel.getQItemForWeaponNamed(theName) + for idx, theItem in pairs (guardianAngel.missilesInTheAir) do + if theItem.weaponName == theName then + return theItem + end + end + return nil +end -- calculate a point in direction from plane (pln) to weapon (wpn), dist meters function guardianAngel.calcSafeExplosionPoint(wpn, pln, dist) @@ -138,6 +211,7 @@ function guardianAngel.calcSafeExplosionPoint(wpn, pln, dist) return newPoint end +--[[-- function guardianAngel.bubbleCheck(wPos, w) if true then return false end for idx, aProtectee in pairs (guardianAngel.unitsToWatchOver) do @@ -154,41 +228,102 @@ function guardianAngel.bubbleCheck(wPos, w) end return false end +--]]-- function guardianAngel.monitorItem(theItem) local w = theItem.theWeapon local ID = theItem.tID if not w then return false end if not w:isExist() then - if (not theItem.missed) and (not theItem.lostTrack) then + --if (not theItem.missed) and (not theItem.lostTrack) then local desc = theItem.weaponName .. ": DISAPPEARED" - if guardianAngel.announcer then + if guardianAngel.announcer and theItem.threat then if guardianAngel.private then trigger.action.outTextForGroup(ID, desc, 30) else trigger.action.outText(desc, 30) end end + if guardianAngel.verbose then + trigger.action.outText("+++gA: missile disappeared: <" .. theItem.weaponName .. ">, aimed at <" .. theItem.targetName .. ">",30) + end + guardianAngel.invokeCallbacks("disappear", theItem.targetName, theItem.weaponName) - end + -- end return false end local t = theItem.theTarget local currentTarget = w:getTarget() - local oldWPos = theItem.wP + + -- Re-target check. did missile pick a new target? + -- this can happen with any missile, even threat missiles, + -- so do this always! + local ctName = nil + if currentTarget then + -- get current name to check against last target name + ctName = currentTarget:getName() + else + -- currentTarget has disappeared, kill the 'threat flag' + -- theItem.threat = false + ctName = "***guardianangel.not.set" + end + + if ctName and ctName ~= theItem.targetName then + if guardianAngel.verbose then + --trigger.action.outText("+++gA: RETARGETING for <" .. theItem.weaponName .. ">: from <" .. theItem.targetName .. "> to <" .. ctName .. ">", 30) + end + + -- see if it's a threat to us now + local watchedUnit = guardianAngel.getWatchedUnitByName(ctName) + + -- update the db who's seeking who + guardianAngel.missilesAndTargets[theItem.weaponName] = ctName + + -- should now update theItem to new target info + isThreat = false + if guardianAngel.getWatchedUnitByName(ctName) then + isThreat = true + if guardianAngel.verbose then + trigger.action.outText("+++gA: <" .. theItem.weaponName .. "> now targeting protected <" .. ctName .. ">!", 30) + end + + if isThreat and guardianAngel.announcer and guardianAngel.active then + local desc = "Missile, missile, missile - now heading for " .. ctName .. "!" + if guardianAngel.private then + trigger.action.outTextForGroup(ID, desc, 30) + else + trigger.action.outText(desc, 30) + end + end + end + guardianAngel.retargetItem(theItem, currentTarget, isThreat) + t = currentTarget + else + -- not ctName, or name as before. + -- go on. + end + + -- we only progress here is the missile is a threat. + -- if not, we keep it and check next time if it has + -- retargeted a protegee + if not theItem.threat then return true end + + -- local oldWPos = theItem.wP local A = w:getPoint() -- A is new point of weapon - theItem.wp = A -- update new position, old is in oldWPos + -- theItem.wp = A -- update new position, old is in oldWPos -- new code: safety check with ALL protected wings - local bubbleThreat = guardianAngel.bubbleCheck(A, w) + -- local bubbleThreat = guardianAngel.bubbleCheck(A, w) + -- safety check removed, no benefit after new code local B if currentTarget then B = currentTarget:getPoint() else B = A end local d = math.floor(dcsCommon.dist(A, B)) + theItem.lastDistance = d -- save it for post mortem local desc = theItem.weaponName .. ": " - if t == currentTarget then + if true or t == currentTarget then desc = desc .. "tracking " .. theItem.targetName .. ", d = " .. d .. "m" local vcc = dcsCommon.getClosingVelocity(t, w) desc = desc .. ", Vcc = " .. math.floor(vcc) .. "m/s" @@ -200,13 +335,15 @@ function guardianAngel.monitorItem(theItem) -- destroy the missile local lethalRange = math.abs(vcc / guardianAngel.ups) * guardianAngel.safetyFactor desc = desc .. ", LR= " .. math.floor(lethalRange) .. "m" + theItem.lastDesc = desc + theItem.timeStamp = timer.getTime() + if guardianAngel.intervention and d <= lethalRange + 10 then desc = desc .. " ANGEL INTERVENTION" - if theItem.lostTrack then desc = desc .. " (little sneak!)" end - if theItem.missed then desc = desc .. " (missed you!)" end - + --if theItem.lostTrack then desc = desc .. " (little sneak!)" end + --if theItem.missed then desc = desc .. " (missed you!)" end if guardianAngel.announcer then if guardianAngel.private then @@ -232,8 +369,8 @@ function guardianAngel.monitorItem(theItem) d <= guardianAngel.minMissileDist -- god's override then desc = desc .. " GOD INTERVENTION" - if theItem.lostTrack then desc = desc .. " (little sneak!)" end - if theItem.missed then desc = desc .. " (missed you!)" end + --if theItem.lostTrack then desc = desc .. " (little sneak!)" end + --if theItem.missed then desc = desc .. " (missed you!)" end if guardianAngel.announcer then if guardianAngel.private then @@ -251,6 +388,7 @@ function guardianAngel.monitorItem(theItem) return false -- remove from list end else + --[[-- if not theItem.lostTrack then desc = desc .. "Missile LOST TRACK" @@ -264,10 +402,12 @@ function guardianAngel.monitorItem(theItem) guardianAngel.invokeCallbacks("trackloss", theItem.targetName, theItem.weaponName) theItem.lostTrack = true end - theItem.lastDistance = d - return true -- true because they can re-acquire! + --]]-- + -- theItem.lastDistance = d + -- return true -- true because they can re-acquire! end + --[[-- if d > theItem.lastDistance then -- this can be wrong because if a missile is launched -- at an angle, it can initially look as if it missed @@ -287,7 +427,8 @@ function guardianAngel.monitorItem(theItem) theItem.lastDistance = d return true -- better not disregard - they can re-acquire! end - + --]]-- + --[[-- if theItem.missed and d < theItem.lastDistance then desc = desc .. " Missile RE-ACQUIRED!" @@ -301,8 +442,9 @@ function guardianAngel.monitorItem(theItem) theItem.missed = false guardianAngel.invokeCallbacks("reacquire", theItem.targetName, theItem.weaponName) end + --]]-- - theItem.lastDistance = d +-- theItem.lastDistance = d return true end @@ -316,7 +458,7 @@ function guardianAngel.monitorMissiles() -- guardianAngel.detectItem(anItem) -- see if the weapon is still in existence - stillAlive = guardianAngel.monitorItem(anItem) + local stillAlive = guardianAngel.monitorItem(anItem) if stillAlive then table.insert(newArray, anItem) end @@ -324,6 +466,31 @@ function guardianAngel.monitorMissiles() guardianAngel.missilesInTheAir = newArray end +function guardianAngel.filterItem(theItem) + local w = theItem.theWeapon + if not w then return false end + if not w:isExist() then + return false + end + return true -- missile still alive +end + +function guardianAngel.filterMissiles() + local newArray = {} -- we collect all still existing missiles here + -- and replace missilesInTheAir with that for next round + for idx, anItem in pairs (guardianAngel.missilesInTheAir) do + -- we now have an item + -- see about detection + -- guardianAngel.detectItem(anItem) + + -- see if the weapon is still in existence + local stillAlive = guardianAngel.filterItem(anItem) + if stillAlive then + table.insert(newArray, anItem) + end + end + guardianAngel.missilesInTheAir = newArray +end -- -- E V E N T P R O C E S S I N G -- @@ -348,22 +515,79 @@ function guardianAngel.postProcessor(event) -- don't do anything for now end +function guardianAngel.getAngelicZoneForUnit(theUnit) + for idx, theZone in pairs(guardianAngel.angelicZones) do + if cfxZones.unitInZone(theUnit, theZone) then + return theZone + end + end + return nil +end + -- event callback from dcsCommon event handler. preProcessor has returned true function guardianAngel.somethingHappened(event) -- when this is invoked, the preprocessor guarantees that -- it's an interesting event and has initiator local ID = event.id local theUnit = event.initiator - local playerName = theUnit:getPlayerName() -- nil if not a player + -- make sure that this is a cat 0 or cat 1 + local playerName = nil + if theUnit.getPlayerName then + playerName = theUnit:getPlayerName() -- nil if not a player + end + + local mustProtect = false if ID == 15 and playerName then -- this is a player created unit if guardianAngel.verbose then - trigger.action.outText("+++gA: unit born " .. theUnit:getName(), 30) + trigger.action.outText("+++gA: player unit born " .. theUnit:getName(), 30) end if guardianAngel.autoAddPlayers then + mustProtect = true + end + + theZone = guardianAngel.getAngelicZoneForUnit(theUnit) + if theZone then + mustProtect = theZone.angelic + if theZone.verbose or guardianAngel.verbose then + trigger.action.outText("+++gA: angelic zone " .. theZone.name .." -- protect: (" .. dcsCommon.bool2YesNo(mustProtect) .. ")", 30) + end + end + + if mustProtect then guardianAngel.addUnitToWatch(theUnit) - end + end + + return + elseif ID == 15 then + -- AI spawn. check if it is an aircraft and in an angelic zone + -- docs say that initiator is object. so let's see if when we + -- get cat, this returns 1 for unit (as it should, so we can get + -- group, or if it's really a unit, which returns 0 for aircraft + local cat = theUnit:getCategory() + --trigger.action.outText("birth event for " .. theUnit:getName() .. " with cat = " .. cat, 30) + if cat ~= 1 then + -- not a unit, bye bye + return + end + local theGroup = theUnit:getGroup() + local gCat = theGroup:getCategory() + if gCat == 0 or gCat == 1 then + --trigger.action.outText("is aircraft cat " .. gCat, 30) + + theZone = guardianAngel.getAngelicZoneForUnit(theUnit) + if theZone then + mustProtect = theZone.angelic + if theZone.verbose or guardianAngel.verbose then + trigger.action.outText("+++gA: angelic zone <" .. theZone.name .."> contains unit <" .. theUnit:getName() .. ">, protect it: " .. dcsCommon.bool2YesNo(mustProtect) .. ".", 30) + end + end + + if mustProtect then + guardianAngel.addUnitToWatch(theUnit) + end + end return end @@ -372,9 +596,23 @@ function guardianAngel.somethingHappened(event) if guardianAngel.verbose then trigger.action.outText("+++gA: player seated in unit " .. theUnit:getName(), 30) end + if guardianAngel.autoAddPlayers then + mustProtect = true + end + + theZone = guardianAngel.getAngelicZoneForUnit(theUnit) + if theZone then + mustProtect = theZone.angelic + if theZone.verbose or guardianAngel.verbose then + trigger.action.outText("+++gA: angelic zone " .. theZone.name .." -- protect: (" .. dcsCommon.bool2YesNo(mustProtect) .. ")", 30) + end + end + + if mustProtect then guardianAngel.addUnitToWatch(theUnit) end + return end @@ -390,6 +628,7 @@ function guardianAngel.somethingHappened(event) if ID == 1 then + -- even if not active, we collect missile data -- someone shot something. see if it is fire directed at me local theWeapon = event.weapon local theTarget @@ -406,11 +645,22 @@ function guardianAngel.somethingHappened(event) -- if we get here, we have weapon aimed at a target local targetName = theTarget:getName() local watchedUnit = guardianAngel.getWatchedUnitByName(targetName) - if not watchedUnit then return end -- fired at some other poor sucker, we don't care + guardianAngel.missilesAndTargets[theWeapon:getName()] = targetName + if not watchedUnit then + -- we may still want to watch this if the missile + -- can be re-targeted + if guardianAngel.verbose then + trigger.action.outText("+++gA: missile <" .. theWeapon:getName() .. "> targeting <" .. targetName .. ">, not a threat", 30) + end + -- add it as no threat + local theQItem = guardianAngel.createQItem(theWeapon, theTarget, false) -- this is not a threat, simply watch for re-target + table.insert(guardianAngel.missilesInTheAir, theQItem) + return + end -- fired at some other poor sucker, we don't care -- if we get here, someone fired a guided weapon at my watched units -- create a new item for my queue - local theQItem = guardianAngel.createQItem(theWeapon, theTarget) -- prob 100 + local theQItem = guardianAngel.createQItem(theWeapon, theTarget, true) -- this is watched table.insert(guardianAngel.missilesInTheAir, theQItem) guardianAngel.invokeCallbacks("launch", theQItem.targetName, theQItem.weaponName) @@ -420,13 +670,17 @@ function guardianAngel.somethingHappened(event) local oclock = dcsCommon.clockPositionOfARelativeToB(A, B, unitHeading) local grpID = theTarget:getGroup():getID() - if guardianAngel.launchWarning then + local vbInfo = "" + if guardianAngel.verbose then + vbInfo = ", <" .. theWeapon:getName() .. "> targeting <" .. targetName .. ">" + end + if guardianAngel.launchWarning and guardianAngel.active then -- currently, we always detect immediately -- can be moved to update() if guardianAngel.private then - trigger.action.outTextForGroup(grpID, "Missile, missile, missile, " .. oclock .. " o clock", 30) + trigger.action.outTextForGroup(grpID, "Missile, missile, missile, " .. oclock .. " o clock" .. vbInfo, 30) else - trigger.action.outText("Missile, missile, missile, " .. oclock .. " o clock", 30) + trigger.action.outText("Missile, missile, missile, " .. oclock .. " o clock" .. vbInfo, 30) end theQItem.detected = true -- remember: we detected and warned already @@ -435,6 +689,7 @@ function guardianAngel.somethingHappened(event) end if ID == 2 then + if not guardianAngel.active then return end -- we aren't on watch. if not guardianAngel.intervention then return end -- we don't intervene if not event.weapon then return end -- no weapon, no interest local theWeapon = event.weapon @@ -445,16 +700,37 @@ function guardianAngel.somethingHappened(event) local theProtegee = nil for idx, aProt in pairs(guardianAngel.unitsToWatchOver) do - if tName == aProt:getName() then - theProtegee = aProt - end - end - + if aProt:isExist() then + if tName == aProt:getName() then + theProtegee = aProt + end + else + if guardianAngel.verbose then + trigger.action.outText("+++gA: whoops. Looks like I lost a wing there... sorry", 30) + end + end + end if not theProtegee then return end -- one of our protegees was hit --trigger.action.outText("+++gA: Protegee " .. tName .. " was hit", 30) - trigger.action.outText("+++gA: I:" .. theUnit:getName() .. " hit " .. tName .. " with " .. wName, 30) + trigger.action.outText("+++gA: I:" .. theUnit:getName() .. " hit " .. tName .. " with " .. wName, 30) -- note: theUnit is the LAUNCHER or the weapon!!! + if guardianAngel.missilesAndTargets[wName] and guardianAngel.verbose then + trigger.action.outText("+++gA: <" .. wName .. "> was originally aimed at <" .. guardianAngel.missilesAndTargets[wName] .. ">", 30) + local qName = guardianAngel.missilesAndTargets[wName] + if qName ~= tName then + trigger.action.outText("+++gA: RETARGET DETECTED", 30) + local wpnTgt = theWeapon:getTarget() + local wpnTgtName = "(none???)" + if wpnTgt then wpnTgtName = wpnTgt:getName() end + trigger.action.outText("+++gA: *current* weapon's target is <" .. wpnTgtName .. ">", 30) + if wpnTgtName ~= tName then + trigger.action.outText("+++gA: COLLATERAL DAMAGE!", 30) + end + end + else + trigger.action.outText("***gA: no missile in the air for <" .. wName .. ">!!!!") + end -- let's see if the victim was in our list of protected -- units local thePerp = nil @@ -479,6 +755,18 @@ function guardianAngel.somethingHappened(event) -- if we should have protected: mea maxima culpa trigger.action.outText("[+++gA: Angel hangs her head in shame. Mea Culpa, " .. tName.."]", 30) + -- see if we can find the q item + local missedItem = guardianAngel.getQItemForWeaponNamed(wName) + if not missedItem then + trigger.action.outText("Cannot retrieve item for <" .. wName .. ">", 30) + else + local now = timer.getTime() + local delta = now - missedItem.timeStamp + local wasThreat = dcsCommon.bool2YesNo(missedItem.threat) + + trigger.action.outText("post: target was <" .. missedItem.targetName .. "> with last dist <" .. missedItem.lastDistance .. "> for weapon <" .. missedItem.weaponName .. ">, with dast desc = <" .. missedItem.lastDesc .. ">, <" .. delta .. "> s ago, Threat:(" .. wasThreat .. ")", 30) + end + return end @@ -495,21 +783,102 @@ end function guardianAngel.update() timer.scheduleFunction(guardianAngel.update, {}, timer.getTime() + 1/guardianAngel.ups) + -- and break off if nothing to do + if not guardianAngel.active then + guardianAngel.filterMissiles() + return + end guardianAngel.monitorMissiles() end +function guardianAngel.doActivate() + guardianAngel.active = true + if guardianAngel.verbose or guardianAngel.announcer then + trigger.action.outText("Guardian Angel has activated", 30) + end +end + +function guardianAngel.doDeActivate() + guardianAngel.active = false + if guardianAngel.verbose or guardianAngel.announcer then + trigger.action.outText("Guardian Angel NO LONGER ACTIVE", 30) + end +end + +function guardianAngel.flagUpdate() + timer.scheduleFunction(guardianAngel.flagUpdate, {}, timer.getTime() + 1) -- once every second + + -- check the flags for on/off + if guardianAngel.activate then + if cfxZones.testZoneFlag(guardianAngel, guardianAngel.activate, "change","lastActivate") then + guardianAngel.doActivate() + end + end + + if guardianAngel.deactivate then + if cfxZones.testZoneFlag(guardianAngel, guardianAngel.deactivate, "change","lastDeActivate") then + guardianAngel.doDeActivate() + end + end +end + function guardianAngel.collectPlayerUnits() -- make sure we have all existing player units -- at start of game - if not guardianAngel.autoAddPlayer then return end +-- if not guardianAngel.autoAddPlayer then return end for i=1, 2 do -- currently only two factions in dcs - factionUnits = coalition.getPlayers(i) - for idx, aPlayerUnit in pairs(factionUnits) do - -- add all existing faction units - guardianAngel.addUnitToWatch(aPlayerUnit) + local factionUnits = coalition.getPlayers(i) + for idx, theUnit in pairs(factionUnits) do + local mustProtect = false + if guardianAngel.autoAddPlayers then + mustProtect = true + end + + theZone = guardianAngel.getAngelicZoneForUnit(theUnit) + if theZone then + mustProtect = theZone.angelic + if theZone.verbose or guardianAngel.verbose then + trigger.action.outText("+++gA: angelic zone " .. theZone.name .." contains player unit <" .. theUnit:getName() .. "> -- protect: (" .. dcsCommon.bool2YesNo(mustProtect) .. ")", 30) + end + end + + if mustProtect then + guardianAngel.addUnitToWatch(theUnit) + end + + end + end +end + +function guardianAngel.collectAIUnits() + -- make sure we have all existing AI units + -- at start of game + for i=1, 2 do + -- currently only two factions in dcs + local factionGroups = coalition.getGroups(i) + for idg, aGroup in pairs(factionGroups) do + local factionUnits = aGroup:getUnits() + for idx, theUnit in pairs(factionUnits) do + local mustProtect = false + + local gCat = aGroup:getCategory() + if gCat == 0 or gCat == 1 then + theZone = guardianAngel.getAngelicZoneForUnit(theUnit) + if theZone then + mustProtect = theZone.angelic + if theZone.verbose or guardianAngel.verbose then + trigger.action.outText("+++gA: angelic zone <" .. theZone.name .."> contains AI unit <" .. theUnit:getName() .. ">, protect it: " .. dcsCommon.bool2YesNo(mustProtect) .. ".", 30) + end + end + + if mustProtect then + guardianAngel.addUnitToWatch(theUnit) + end + end + end end end end @@ -520,14 +889,11 @@ end function guardianAngel.readConfigZone() -- note: must match exactly!!!! local theZone = cfxZones.getZoneByName("guardianAngelConfig") - if not theZone then - trigger.action.outText("+++gA: no config zone!", 30) - return - end - if guardianAngel.verbose then - trigger.action.outText("+++gA: found config zone!", 30) + if not theZone then + theZone = cfxZones.createSimpleZone("guardianAngelConfig") end + guardianAngel.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) guardianAngel.autoAddPlayer = cfxZones.getBoolFromZoneProperty(theZone, "autoAddPlayer", true) @@ -537,8 +903,52 @@ function guardianAngel.readConfigZone() guardianAngel.private = cfxZones.getBoolFromZoneProperty(theZone, "private", false) guardianAngel.explosion = cfxZones.getNumberFromZoneProperty(theZone, "explosion", -1) guardianAngel.fxDistance = cfxZones.getNumberFromZoneProperty(theZone, "fxDistance", 500) + + guardianAngel.active = cfxZones.getBoolFromZoneProperty(theZone, "active", true) + + if cfxZones.hasProperty(theZone, "activate?") then + guardianAngel.activate = cfxZones.getStringFromZoneProperty(theZone, "activate?", "*") + guardianAngel.lastActivate = cfxZones.getFlagValue(guardianAngel.activate, theZone) + elseif cfxZones.hasProperty(theZone, "on?") then + guardianAngel.activate = cfxZones.getStringFromZoneProperty(theZone, "on?", "*") + guardianAngel.lastActivate = cfxZones.getFlagValue(guardianAngel.activate, theZone) + end + + if cfxZones.hasProperty(theZone, "deactivate?") then + guardianAngel.deactivate = cfxZones.getStringFromZoneProperty(theZone, "deactivate?", "*") + guardianAngel.lastDeActivate = cfxZones.getFlagValue(guardianAngel.deactivate, theZone) + elseif cfxZones.hasProperty(theZone, "off?") then + guardianAngel.deactivate = cfxZones.getStringFromZoneProperty(theZone, "off?", "*") + guardianAngel.lastDeActivate = cfxZones.getFlagValue(guardianAngel.deactivate, theZone) + end + + guardianAngel.configZone = theZone + if guardianAngel.verbose then + trigger.action.outText("+++gA: processed config zone", 30) + end end +-- +-- guardian zones +-- + +function guardianAngel.processGuardianZone(theZone) + theZone.angelic = cfxZones.getBoolFromZoneProperty(theZone, "guardian", true) + + + if theZone.verbose or guardianAngel.verbose then + trigger.action.outText("+++gA: processed 'guardian' zone <" .. theZone.name .. ">", 30) + end + -- add it to my angelicZones + table.insert(guardianAngel.angelicZones, theZone) +end + +function guardianAngel.readGuardianZones() + local attrZones = cfxZones.getZonesWithAttributeNamed("guardian") + for k, aZone in pairs(attrZones) do + guardianAngel.processGuardianZone(aZone) + end +end -- -- start @@ -553,6 +963,9 @@ function guardianAngel.start() -- read config guardianAngel.readConfigZone() + -- read guarded zones + guardianAngel.readGuardianZones() + -- install event monitor dcsCommon.addEventHandler(guardianAngel.somethingHappened, guardianAngel.preProcessor, @@ -560,10 +973,14 @@ function guardianAngel.start() -- collect all units that are already in the game at this point guardianAngel.collectPlayerUnits() + guardianAngel.collectAIUnits() -- start update guardianAngel.update() + -- start flag check + guardianAngel.flagUpdate() + trigger.action.outText("Guardian Angel v" .. guardianAngel.version .. " running", 30) return true end @@ -581,3 +998,9 @@ end -- test callback --guardianAngel.addCallback(guardianAngel.testCB) --guardianAngel.invokeCallbacks("A", "B", "C") + +--[[-- +to do + - turn on and off via flags + - zones that designate protected/unprotected aircraft + --]]-- \ No newline at end of file diff --git a/modules/messenger.lua b/modules/messenger.lua index 5a499c2..c8e8b62 100644 --- a/modules/messenger.lua +++ b/modules/messenger.lua @@ -1,5 +1,5 @@ messenger = {} -messenger.version = "1.3.1" +messenger.version = "1.3.2" messenger.verbose = false messenger.requiredLibs = { "dcsCommon", -- always @@ -22,7 +22,9 @@ messenger.messengers = {} 1.2.1 - qoL: = newline, = zone name, = value 1.3.0 - messenger? saves messageOut? attribute 1.3.1 - message now can interpret value as time with <:h> <:m> <:s> - + 1.3.2 - message interprets as time in HH:MM:SS of current time + - can interpret , , + - zone-local verbosity --]]-- function messenger.addMessenger(theZone) @@ -54,6 +56,24 @@ function messenger.preProcMessage(inMsg, theZone) 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 = cfxZones.getPoint(theZone) + local lat, lon, alt = coord.LOtoLL(currPoint) + lat, lon = dcsCommon.latLon2Text(lat, lon) + outMsg = outMsg:gsub("", lat) + outMsg = outMsg:gsub("", lon) + local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) + local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing + outMsg = outMsg:gsub("", mgrs) return outMsg end @@ -133,7 +153,7 @@ function messenger.createMessengerWithZone(theZone) theZone.messageValue = cfxZones.getStringFromZoneProperty(theZone, "messageValue?", "") end - if messenger.verbose then + if messenger.verbose or theZone.verbose then trigger.action.outText("+++Msg: new zone <".. theZone.name .."> will say <".. theZone.message .. ">", 30) end end @@ -167,7 +187,7 @@ end function messenger.isTriggered(theZone) -- this module has triggered if theZone.messageOff then - if messenger.verbose then + if messenger.verbose or theZone.verbose then trigger.action.outFlag("msg: message for <".. theZone.name .."> is OFF",30) end return @@ -175,7 +195,7 @@ function messenger.isTriggered(theZone) local fileName = "l10n/DEFAULT/" .. theZone.soundFile local msg = messenger.getMessage(theZone) - if messenger.verbose then + if messenger.verbose or theZone.verbose then trigger.action.outText("+++Msg: <".. theZone.name .."> will say <".. msg .. ">", 30) end @@ -200,7 +220,7 @@ function messenger.update() -- make sure to re-start before reading time limit -- new trigger code if cfxZones.testZoneFlag(aZone, aZone.triggerMessagerFlag, aZone.msgTriggerMethod, "lastMessageTriggerValue") then - if messenger.verbose then + if messenger.verbose or aZone.verbose then trigger.action.outText("+++msgr: triggered on in? for <".. aZone.name ..">", 30) end messenger.isTriggered(aZone) @@ -209,14 +229,14 @@ function messenger.update() -- old trigger code if cfxZones.testZoneFlag(aZone, aZone.messageOffFlag, aZone.msgTriggerMethod, "lastMessageOff") then aZone.messageOff = true - if messenger.verbose then + if messenger.verbose or aZone.verbose then trigger.action.outText("+++msg: messenger <" .. aZone.name .. "> turned ***OFF***", 30) end end if cfxZones.testZoneFlag(aZone, aZone.messageOnFlag, aZone.msgTriggerMethod, "lastMessageOn") then aZone.messageOff = false - if messenger.verbose then + if messenger.verbose or aZone.verbose then trigger.action.outText("+++msg: messenger <" .. aZone.name .. "> turned ON", 30) end end diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index a4be3b6..b49701c 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -1,5 +1,5 @@ radioMenu = {} -radioMenu.version = "1.0.0" +radioMenu.version = "1.0.1" radioMenu.verbose = false radioMenu.ups = 1 radioMenu.requiredLibs = { @@ -11,6 +11,7 @@ radioMenu.menus = {} --[[-- Version History 1.0.0 Initial version + 1.0.1 spelling corrections --]]-- function radioMenu.addRadioMenu(theZone) @@ -181,7 +182,7 @@ function radioMenu.doMenuX(args) cfxZones.pollFlag(theFlag, theZone.radioMethod, theZone) if theZone.verbose or radioMenu.verbose then - trigger.action.outText("+++menu: banging D! with <" .. theZone.radioMethod .. "> on <" .. theFlag .. "> for " .. theZone.name, 30) + trigger.action.outText("+++menu: banging with <" .. theZone.radioMethod .. "> on <" .. theFlag .. "> for " .. theZone.name, 30) end end diff --git a/tutorial & demo missions/demo - 000 smoke em! DML intro.miz b/tutorial & demo missions/demo - 000 smoke em! DML intro.miz index 76f2c58..14f3c1c 100644 Binary files a/tutorial & demo missions/demo - 000 smoke em! DML intro.miz and b/tutorial & demo missions/demo - 000 smoke em! DML intro.miz differ diff --git a/tutorial & demo missions/demo - artillery recon.miz b/tutorial & demo missions/demo - artillery recon.miz new file mode 100644 index 0000000..fc76486 Binary files /dev/null and b/tutorial & demo missions/demo - artillery recon.miz differ diff --git a/tutorial & demo missions/demo - missile evasion (guardian angel).miz b/tutorial & demo missions/demo - missile evasion (guardian angel).miz index 18f501f..ac5553d 100644 Binary files a/tutorial & demo missions/demo - missile evasion (guardian angel).miz and b/tutorial & demo missions/demo - missile evasion (guardian angel).miz differ diff --git a/tutorial & demo missions/demo - recon mode - reloaded.miz b/tutorial & demo missions/demo - recon mode - reloaded.miz new file mode 100644 index 0000000..9c176bf Binary files /dev/null and b/tutorial & demo missions/demo - recon mode - reloaded.miz differ diff --git a/tutorial & demo missions/demo - recon mode.miz b/tutorial & demo missions/demo - recon mode.miz index f1f8153..ee75bc8 100644 Binary files a/tutorial & demo missions/demo - recon mode.miz and b/tutorial & demo missions/demo - recon mode.miz differ