Version 2.2.6

Reaper and Kiowa synch, sweeper
This commit is contained in:
Christian Franz 2024-06-20 12:52:21 +02:00
parent 2b6491b978
commit 0aca69967c
13 changed files with 415 additions and 213 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
autoCSAR = {} autoCSAR = {}
autoCSAR.version = "2.1.0" autoCSAR.version = "2.2.0"
autoCSAR.requiredLibs = { autoCSAR.requiredLibs = {
"dcsCommon", -- always "dcsCommon", -- always
"cfxZones", -- Zones, of course "cfxZones", -- Zones, of course
@ -17,7 +17,26 @@ autoCSAR.trackedEjects = {} -- we start tracking on eject
2.0.1 - fix for coalition change when ejected player changes coas or is forced to neutral 2.0.1 - fix for coalition change when ejected player changes coas or is forced to neutral
- GC - GC
2.1.0 - persistence support 2.1.0 - persistence support
2.2.0 - new noExploit option in config
- no csar mission if pilot lands too close to airbase or farp
and noExploit is on
--]]-- --]]--
autoCSAR.forbidden = {} -- indexed by name, contains point
autoCSAR.killDist = 2100 -- meters from center of forbidden
function autoCSAR.collectForbiddenZones()
local allYourBase = world.getAirbases()
for idx, aBase in pairs(allYourBase) do
-- collect airbases and farps, ignore ships
local desc = aBase:getDesc()
local cat = desc.category
if cat == 0 or cat == 1 then
local name = aBase:getName()
local p = aBase:getPoint()
autoCSAR.forbidden[name] = p
end
end
end
function autoCSAR.removeGuy(args) function autoCSAR.removeGuy(args)
local theGuy = args.theGuy local theGuy = args.theGuy
@ -53,6 +72,28 @@ function autoCSAR.createNewCSAR(theUnit, coa)
if coa == 2 and not autoCSAR.blueCSAR then if coa == 2 and not autoCSAR.blueCSAR then
return -- no blue rescue return -- no blue rescue
end end
-- noExploit burnup
if autoCSAR.noExploit then
local p = theUnit:getPoint()
local burned = false
for name, aPoint in pairs(autoCSAR.forbidden) do
local d = dcsCommon.distFlat(p, aPoint)
if d < autoCSAR.killDist then
if autoCSAR.verbose then
trigger.action.outText("+++aCSAR: BURNED ejection touchdown: too close to <" .. name .. ">", 30)
end
burned = true
end
end
if burned then
trigger.action.outText("Pilot made it safely to ground, and was taken into custody immediately", 30)
-- try and remove the guy now
Unit.destroy(theUnit)
return
end
end
-- end burnup code
-- for later expansion -- for later expansion
local theGroup = theUnit:getGroup() local theGroup = theUnit:getGroup()
@ -130,7 +171,6 @@ function autoCSAR:onEvent(event)
if event.id == 6 then -- eject, start tracking, remember coa if event.id == 6 then -- eject, start tracking, remember coa
local coa = event.initiator:getCoalition() local coa = event.initiator:getCoalition()
-- see if pilot has ejector seat and prepare to connect one with the other -- see if pilot has ejector seat and prepare to connect one with the other
local info = nil local info = nil
if event.target and event.target:isExist() then if event.target and event.target:isExist() then
@ -161,9 +201,6 @@ function autoCSAR.readConfigZone()
local theZone = cfxZones.getZoneByName("autoCSARConfig") local theZone = cfxZones.getZoneByName("autoCSARConfig")
if not theZone then if not theZone then
theZone = cfxZones.createSimpleZone("autoCSARConfig") theZone = cfxZones.createSimpleZone("autoCSARConfig")
if autoCSAR.verbose then
trigger.action.outText("+++aCSAR: NO config zone!", 30)
end
end end
autoCSAR.verbose = theZone.verbose autoCSAR.verbose = theZone.verbose
autoCSAR.redCSAR = theZone:getBoolFromZoneProperty("red", true) autoCSAR.redCSAR = theZone:getBoolFromZoneProperty("red", true)
@ -178,6 +215,9 @@ function autoCSAR.readConfigZone()
autoCSAR.seaCSAR = theZone:getBoolFromZoneProperty("seaCSAR", true) autoCSAR.seaCSAR = theZone:getBoolFromZoneProperty("seaCSAR", true)
autoCSAR.noExploit = theZone:getBoolFromZoneProperty("noExploit", false)
autoCSAR.killDist = theZone:getNumberFromZoneProperty("killDist", 2100)
if autoCSAR.verbose then if autoCSAR.verbose then
trigger.action.outText("+++aCSAR: read config", 30) trigger.action.outText("+++aCSAR: read config", 30)
end end
@ -249,6 +289,9 @@ function autoCSAR.start()
autoCSAR.loadData() autoCSAR.loadData()
end end
-- collect forbidden zones if noExploit is active
autoCSAR.collectForbiddenZones()
-- start GC -- start GC
timer.scheduleFunction(autoCSAR.GC, {}, timer.getTime() + 1) timer.scheduleFunction(autoCSAR.GC, {}, timer.getTime() + 1)

View File

@ -2189,4 +2189,6 @@ end
nameTest - optional safety / debug feature that will name-test each unit that is about to be spawned for replacement. Maybe auto turn on when verbose is set? nameTest - optional safety / debug feature that will name-test each unit that is about to be spawned for replacement. Maybe auto turn on when verbose is set?
make example where transport can be different plane types but have same name make example where transport can be different plane types but have same name
support 'orders' to complete replace routes, and pass to groundCommander like spawner. only for ground troops
--]]-- --]]--

View File

@ -4,7 +4,7 @@
-- *** EXTENDS ZONES: 'pathing' attribute -- *** EXTENDS ZONES: 'pathing' attribute
-- --
cfxCommander = {} cfxCommander = {}
cfxCommander.version = "2.0.0" cfxCommander.version = "2.0.1"
--[[-- VERSION HISTORY --[[-- VERSION HISTORY
- 1.0.5 - createWPListForGroupToPointViaRoads: detect no road found - 1.0.5 - createWPListForGroupToPointViaRoads: detect no road found
- 1.0.6 - build in more group checks in assign wp list - 1.0.6 - build in more group checks in assign wp list
@ -35,6 +35,7 @@ cfxCommander.version = "2.0.0"
- hardened performCommands() - hardened performCommands()
- createWPListForGroupToPoint() supports moveFormation - createWPListForGroupToPoint() supports moveFormation
- makeGroupGoTherePreferringRoads() supports moveFormation - makeGroupGoTherePreferringRoads() supports moveFormation
- 2.0.1 - hardened createBasicWaypoint when no formation given (default to echelonR
--]]-- --]]--
cfxCommander.requiredLibs = { cfxCommander.requiredLibs = {
@ -260,6 +261,8 @@ function cfxCommander.createBasicWaypoint(point, speed, formation)
if not speed then speed = 6 end -- 6 m/s = 20 kph if not speed then speed = 6 end -- 6 m/s = 20 kph
wp.speed = speed wp.speed = speed
if not formation then formation = "EchelonR" end
if cfxCommander.forceOffRoad then if cfxCommander.forceOffRoad then
formation = "Off Road" formation = "Off Road"
end end

View File

@ -1,5 +1,5 @@
reaper = {} reaper = {}
reaper.version = "1.0.0" reaper.version = "1.1.0"
reaper.requiredLibs = { reaper.requiredLibs = {
"dcsCommon", "dcsCommon",
"cfxZones", "cfxZones",
@ -8,7 +8,20 @@ reaper.requiredLibs = {
VERSION HISTORY VERSION HISTORY
1.0.0 - Initial Version 1.0.0 - Initial Version
1.1.0 - Individual status
- cycle target method
- cycle? attribute
- restructured menus
- added cycle target
- single status reprots full group
- drones have AFAC task instead of Reconnaissance
- Setting enroute task for group once target spotted
- compatible with Kiowa's L2MUM
- (undocumented) freq attribute for drones (in MHz)
- completely rewrote scanning method (performance)
- added FAC task
- split task generation from wp generation
- updated reaper naming, uniqueNames attribute (undocumented)
--]]-- --]]--
@ -17,6 +30,12 @@ reaper.scanning = {} -- zones that are scanning (looking for tgt). by zone name
reaper.tracking = {} -- zones that are tracking tgt. by zone name reaper.tracking = {} -- zones that are tracking tgt. by zone name
reaper.scanInterval = 10 -- seconds reaper.scanInterval = 10 -- seconds
reaper.trackInterval = 0.3 -- seconds reaper.trackInterval = 0.3 -- seconds
reaper.uuidCnt = 0
function reaper.uuid(instring)
reaper.uuidCnt = reaper.uuidCnt + 1
return instring .. "-R" .. reaper.uuidCnt
end
-- reading reaper zones -- reading reaper zones
function reaper.readReaperZone(theZone) function reaper.readReaperZone(theZone)
@ -25,6 +44,7 @@ function reaper.readReaperZone(theZone)
if theZone.myType == "MQ-9 Reaper" then if theZone.myType == "MQ-9 Reaper" then
theZone.alt = 9500 theZone.alt = 9500
else theZone.alt = 7500 end theZone.alt = theZone:getNumberFromZoneProperty("alt", theZone.alt) else theZone.alt = 7500 end theZone.alt = theZone:getNumberFromZoneProperty("alt", theZone.alt)
theZone.freq = theZone:getNumberFromZoneProperty("freq", 133) * 1000000 -- in MHz
theZone.coa = theZone:getCoalitionFromZoneProperty("coalition", 2) theZone.coa = theZone:getCoalitionFromZoneProperty("coalition", 2)
if theZone.coa == 0 then if theZone.coa == 0 then
trigger.action.outText("+++Reap: Zone <" .. theZone.name .. "> is of coalition NEUTRAL. Switched to BLUE", 30) trigger.action.outText("+++Reap: Zone <" .. theZone.name .. "> is of coalition NEUTRAL. Switched to BLUE", 30)
@ -44,6 +64,7 @@ function reaper.readReaperZone(theZone)
theZone.autoRespawn = theZone:getBoolFromZoneProperty("autoRespawn", false) theZone.autoRespawn = theZone:getBoolFromZoneProperty("autoRespawn", false)
theZone.launchUI = theZone:getBoolFromZoneProperty("launchUI", true) theZone.launchUI = theZone:getBoolFromZoneProperty("launchUI", true)
theZone.statusUI = theZone:getBoolFromZoneProperty("statusUI", true) theZone.statusUI = theZone:getBoolFromZoneProperty("statusUI", true)
theZone.uniqueNames = theZone:getBoolFromZoneProperty("uniqueNames", true) -- undocumented, leave true
if theZone:hasProperty("launch?") then if theZone:hasProperty("launch?") then
theZone.launch = theZone:getStringFromZoneProperty("launch?", "<none>") theZone.launch = theZone:getStringFromZoneProperty("launch?", "<none>")
theZone.launchVal = theZone:getFlagValue(theZone.launch) theZone.launchVal = theZone:getFlagValue(theZone.launch)
@ -52,6 +73,11 @@ function reaper.readReaperZone(theZone)
theZone.status = theZone:getStringFromZoneProperty("status?", "<none>") theZone.status = theZone:getStringFromZoneProperty("status?", "<none>")
theZone.statusVal = theZone:getFlagValue(theZone.status) theZone.statusVal = theZone:getFlagValue(theZone.status)
end end
if theZone:hasProperty("cycle?") then
theZone.cycle = theZone:getStringFromZoneProperty("cycle?", "<none>")
theZone.cycleVal = theZone:getFlagValue(theZone.cycle)
end
theZone.hasSpawned = false theZone.hasSpawned = false
if theZone.onStart then if theZone.onStart then
@ -61,10 +87,24 @@ end
-- spawn a drone from a zone -- spawn a drone from a zone
function reaper.spawnForZone(theZone, ack) function reaper.spawnForZone(theZone, ack)
-- delete any group with the same name
if not theZone.uniqueNames then
local exister = Group.getByName(theZone.name)
if exister then Group.destroy(exister) end
end
-- create spawn data -- create spawn data
local gdata = dcsCommon.createEmptyGroundGroupData (dcsCommon.uuid(theZone.name)) local rName
gdata.task = "Reconnaissance" if theZone.uniqueNames then
rName = reaper.uuid(theZone.name)
else
rName = theZone.name
end
local gdata = dcsCommon.createEmptyGroundGroupData (rName) -- warning: non-unique unit names, will replace previous
gdata.task = "AFAC"
gdata.route = {} gdata.route = {}
-- calculate left and right -- calculate left and right
local p = theZone:getPoint() local p = theZone:getPoint()
local left, right local left, right
@ -78,23 +118,31 @@ function reaper.spawnForZone(theZone, ack)
end end
gdata.x = left.x gdata.x = left.x
gdata.y = left.z gdata.y = left.z
gdata.frequency = theZone.freq
-- build the unit data -- build the unit data
local unit = {} local unit = {}
unit.name = dcsCommon.uuid(theZone.name) unit.name = rName -- same as group
unit.x = left.x unit.x = left.x
unit.y = left.z unit.y = left.z
unit.type = theZone.myType unit.type = theZone.myType
unit.skill = "High" unit.skill = "High"
if theZone.myType == "MQ-9 Reaper" then if theZone.myType == "MQ-9 Reaper" then
unit.speed = 55 unit.speed = 55
-- unit.alt = 9500
else else
-- unit.alt = 7500
unit.speed = 33 unit.speed = 33
end end
unit.alt = theZone.alt unit.alt = theZone.alt
if theZone.uniqueNames then
else
if theZone.reaperGID and theZone.reaperUID then -- also re-use groupID
gdata.groupId = theZone.reaperGID
unit.unitId = theZone.reaperUID
trigger.action.outText("re-using data from old <" .. theZone.name .. ">", 30)
end
end
-- add to group -- add to group
gdata.units[1] = unit gdata.units[1] = unit
@ -112,7 +160,8 @@ function reaper.spawnForZone(theZone, ack)
trigger.action.outText("+++Reap: failed to spawn for zone <" .. theZone.name .. ">", 30) trigger.action.outText("+++Reap: failed to spawn for zone <" .. theZone.name .. ">", 30)
return return
end end
theZone.reaperGID = theGroup:getID()
theZone.reaperUID = theGroup:getUnit(1):getID()
if theZone.verbose or reaper.verbose then if theZone.verbose or reaper.verbose then
trigger.action.outText("+++reap: Spawned <" .. theGroup:getName() .. "> reaper", 30) trigger.action.outText("+++reap: Spawned <" .. theGroup:getName() .. "> reaper", 30)
end end
@ -145,24 +194,25 @@ function reaper.cleanUp(theZone)
theZone.theSpot = nil theZone.theSpot = nil
end end
function reaper.createInitialWP(p, alt, speed) function reaper.createReaperTask(alt, speed, target, theZone)
local wp = { local task = {
["alt"] = alt,
["action"] = "Turning Point",
["alt_type"] = "BARO",
["properties"] = {
["addopt"] = {}, -- end of ["addopt"]
}, -- end of ["properties"]
["speed"] = speed,
["task"] = {
["id"] = "ComboTask", ["id"] = "ComboTask",
["params"] = { ["params"] = {
["tasks"] = { ["tasks"] = {
[1] = { [1] = {
["enabled"] = true, ["enabled"] = true,
["auto"] = true, ["auto"] = true,
["id"] = "WrappedAction", ["id"] = "FAC",
["number"] = 1, ["number"] = 1,
["params"] =
{}, -- end of ["params"]
}, -- end of [1]
[2] = {
["enabled"] = true,
["auto"] = true,
["id"] = "WrappedAction",
["number"] = 2,
["params"] = { ["params"] = {
["action"] = { ["action"] = {
["id"] = "EPLRS", ["id"] = "EPLRS",
@ -172,21 +222,56 @@ function reaper.createInitialWP(p, alt, speed)
}, -- end of ["params"] }, -- end of ["params"]
}, -- end of ["action"] }, -- end of ["action"]
}, -- end of ["params"] }, -- end of ["params"]
}, -- end of [1] }, -- end of [2]
[2] = { [3] = {
["enabled"] = true, ["enabled"] = true,
["auto"] = false, ["auto"] = false,
["id"] = "Orbit", ["id"] = "Orbit",
["number"] = 2, ["number"] = 3,
["params"] = { ["params"] = {
["altitude"] = alt, ["altitude"] = alt,
["pattern"] = "Race-Track", ["pattern"] = "Race-Track",
["speed"] = speed, ["speed"] = speed,
}, -- end of ["params"] }, -- end of ["params"]
}, -- end of [2] }, -- end of [3]
}, -- end of ["tasks"] }, -- end of ["tasks"]
}, -- end of ["params"] }, -- end of ["params"]
}, -- end of ["task"] } -- end of ["task"]
if theTarget and theZone then
-- local gID = theTarget:getGroup():getID()
local gID = theTarget:getID() -- NOTE: theTarget is a GROUP!!!!
local task4 = {
["enabled"] = true,
["auto"] = false,
["id"] = "FAC_AttackGroup",
["number"] = 4,
["params"] =
{
["number"] = 1,
["designation"] = "No",
["modulation"] = 0,
["groupId"] = gID,
-- ["callname"] = 1,
-- ["datalink"] = true,
["weaponType"] = 0, -- 9663676414,
["frequency"] = theZone.freq, -- 133000000,
}, -- end of ["params"]
} -- end of [4]
task.params.tasks[4] = task4
end
return task
end
function reaper.createInitialWP(p, alt, speed) -- warning: target must be a GROUP
local wp = {
["alt"] = alt,
["action"] = "Turning Point",
["alt_type"] = "BARO",
["properties"] = {
["addopt"] = {}, -- end of ["addopt"]
}, -- end of ["properties"]
["speed"] = speed,
["task"] = {}, -- will construct later
["type"] = "Turning Point", ["type"] = "Turning Point",
["ETA"] = 0, ["ETA"] = 0,
["ETA_locked"] = true, ["ETA_locked"] = true,
@ -195,50 +280,13 @@ function reaper.createInitialWP(p, alt, speed)
["speed_locked"] = true, ["speed_locked"] = true,
["formation_template"] = "", ["formation_template"] = "",
} -- end of wp } -- end of wp
wp.task = reaper.createReaperTask(alt, speed) -- no zone, no target
return wp return wp
end end
-- scanning & tracking function reaper.setTarget(theZone, theTarget, cycled)
-- scanning looks for vehicles to track, and exectues much less often -- add a laser tracker to this unit
-- tracking tracks a single vehicle and places a pointer on it
function reaper.findFirstEnemyUnitVisible(enemies, theZone)
local p = theZone.theUav:getPoint()
-- we assume a flat altitude of 7000m
local visRange = theZone.alt * 1 -- based on tan(45) = 1 --> range = alt
for idx, aGroup in pairs(enemies) do
local theUnits = aGroup:getUnits()
-- optimization: only scan the first vehicle in group if it's in range local theUnit = theUnits[1]
local theUnit = theUnits[1]
if theUnit and Unit.isExist(theUnit) then
up = theUnit:getPoint()
d = dcsCommon.distFlat(up, p)
if d < visRange then
-- try each unit if it is visible from drone
for idy, aUnit in pairs(theUnits) do
local up = aUnit:getPoint()
up.y = up.y + 2
if land.isVisible(p, up) then return aUnit end
end
end
end
end
end
function reaper.scan()
-- how far can the drone see? we calculate with a 120 degree opening
-- camera lens, making half angle = 45 --> tan(45) = 1
-- so the radius of the visible circle on the ground is 1 * altidude
timer.scheduleFunction(reaper.scan, {}, timer.getTime() + reaper.scanInterval)
filtered = {}
local redEnemies = coalition.getGroups(2, 2) -- blue ground vehicles
local blueEnemeis = coalition.getGroups(1, 2) -- get ground vehicles
for name, theZone in pairs(reaper.scanning) do
local enemies = redEnemies
if theZone.coa == 2 then enemies = blueEnemeis end
if Unit.isExist(theZone.theUav) then
local theTarget = reaper.findFirstEnemyUnitVisible(enemies, theZone)
if theTarget then
-- add a laser tracker to this unit
local lp = theTarget:getPoint() local lp = theTarget:getPoint()
local lat, lon, alt = coord.LOtoLL(lp) local lat, lon, alt = coord.LOtoLL(lp)
lat, lon = dcsCommon.latLon2Text(lat, lon) lat, lon = dcsCommon.latLon2Text(lat, lon)
@ -255,9 +303,61 @@ function reaper.scan()
end end
theZone.theSpot = theSpot theZone.theSpot = theSpot
-- put me in track mode -- put me in track mode
reaper.tracking[name] = theZone reaper.tracking[theZone.name] = theZone
if cycled then return end -- cycling inside group, no new tasking
-- now make tracking the group the drone's task
local theGroup = theTarget:getGroup()
local theTask = reaper.createReaperTask(theZone.alt, theZone.speed, theGroup, theZone) -- create full FAC task with orbit and group engage
local theController = theZone.theUav:getController()
if not theController then
trigger.action.outText("+++Rpr: UAV has no controller, getting group")
return
end
theController:setTask(theTask) -- replace with longer task
end
function reaper.selectFromDetectedTargets(visTargets, theZone)
-- use (permanent?) detectedTargetList
for idx, tData in pairs(visTargets) do
if tData then
local theTarget = tData.object
local nn = theTarget:getName()
if not nn or nn == "" then
trigger.action.outText("+++reaper: shortcut on startup", 30)
return nil
end
if theTarget and theTarget.getGroup then -- it's not a group or static object
local d = theTarget:getDesc()
if d.category == 2 then
if theZone.verbose then
trigger.action.outText("+++reap: identified <" .. tData.object:getName() .. "> as target for <" .. theZone.name .. ">")
end
return tData.object
end
end
end
end
return nil
end
function reaper.scanALT() -- alternative, more efficient (?) method using unit's controller
timer.scheduleFunction(reaper.scanALT, {}, timer.getTime() + reaper.scanInterval)
local filtered = {}
for name, theZone in pairs(reaper.scanning) do
local theUAV = theZone.theUav
if Unit.isExist(theUAV) then
-- get the controller
local theController = theUAV:getController()
local visTargets = theController:getDetectedTargets(1, 2)
local theTarget = reaper.selectFromDetectedTargets(visTargets, theZone)
if theTarget then
-- add a laser tracker to this unit
reaper.setTarget(theZone, theTarget)
else else
-- will scan again -- will scan again next round
filtered[name] = theZone filtered[name] = theZone
end end
else else
@ -276,6 +376,7 @@ function reaper.scan()
reaper.scanning = filtered reaper.scanning = filtered
end end
function reaper.track() function reaper.track()
local filtered = {} local filtered = {}
for name, theZone in pairs(reaper.tracking) do for name, theZone in pairs(reaper.tracking) do
@ -310,27 +411,73 @@ function reaper.track()
timer.scheduleFunction(reaper.track, {}, timer.getTime() + reaper.trackInterval) timer.scheduleFunction(reaper.track, {}, timer.getTime() + reaper.trackInterval)
end end
function reaper.cycleTarget(theZone)
local coa = theZone.coa
-- try and advance to the next target
if not theZone.theUav or not Unit.isExist(theZone.theUav) then
trigger.action.outTextForCoalition(coa, "Reaper <" .. theZone.name .. "> not on station, requries launch first", 30)
trigger.action.outSoundForCoalition(theZone.coa, reaper.actionSound)
return
end
if not theZone.theSpot or
not theZone.theUav or
not theZone.theTarget then
trigger.action.outTextForCoalition(coa, "Reaper <" .. theZone.name .. "> is not tracking a target", 30)
trigger.action.outSoundForCoalition(theZone.coa, reaper.actionSound)
return
end
--when we get here, the reaper is tracking a target. get it's group
local theUnit = theZone.theTarget
if not theUnit.getGroup then return end -- safety first
local theGroup = theUnit:getGroup()
local allTargets = theGroup:getUnits()
local filtered = {}
local i = 1
local tIndex = 1
-- filter and find the target with it's index
for idx, aTgt in pairs(allTargets) do
if Unit.isExist(aTgt) then
if theUnit == aTgt then
if theZone.verbose then
trigger.action.outText("+++ reaper <" .. theZone.target .. ">: target index found : <" .. i .. ">", 30)
end
tIndex = i
end
table.insert(filtered, aTgt)
i = i + 1
end
end
local num = #filtered
if num < 2 then
-- nothing to do, simply ack
trigger.action.outTextForCoalition(coa, "<" .. theZone.name .. ">: Only one target left.", 30)
trigger.action.outSoundForCoalition(theZone.coa, reaper.actionSound)
return
end
-- increase tIndex
tIndex = tIndex + 1
if tIndex > #filtered then tIndex = 1 end
local newTarget = filtered[tIndex]
-- tell zone to target this new target
reaper.setTarget(theZone, newTarget, true) -- also outputs text and action sound, true = cycled
end
function reaper.update() function reaper.update()
timer.scheduleFunction(reaper.update, {}, timer.getTime() + 1) timer.scheduleFunction(reaper.update, {}, timer.getTime() + 1)
-- go through all my zones, and respawn those that have no -- go through all my zones, and respawn those that have no
-- uav but have autoRespawn active -- uav but have autoRespawn active
for name, theZone in pairs(reaper.zones) do for name, theZone in pairs(reaper.zones) do
if theZone.autoRespawn and not theZone.theUav and theZone.hasSpawned then if theZone.autoRespawn and not theZone.theUav and theZone.hasSpawned then
-- auto-respawn needs to kick in -- auto-respawn needs to kick in
reaper.scanning[name] = nil reaper.scanning[name] = nil
reaper.tracking[name] = nil reaper.tracking[name] = nil
if reaper.verbose or theZone.verbose then
trigger.action.outText("+++reap: respawning for <" .. name .. ">", 30)
end
reaper.spawnForZone(theZone) reaper.spawnForZone(theZone)
end end
if theZone.status and theZone:testZoneFlag(theZone.status, "change", "statusVal") then if theZone.status and theZone:testZoneFlag(theZone.status, "change", "statusVal") then
if theZone.verbose then
trigger.action.outText("+++reap: Triggered status for zone <" .. name .. "> on <" .. theZone.status .. ">", 30)
end
reaper.doSingleDroneStatus(theZone) reaper.doSingleDroneStatus(theZone)
end end
@ -340,6 +487,10 @@ function reaper.update()
args[2] = name -- = args[2] args[2] = name -- = args[2]
reaper.doLaunch(args) reaper.doLaunch(args)
end end
if theZone.cycle and theZone:testZoneFlag(theZone.cycle, "change", "cycleVal") then
reaper.cycleTarget(theZone)
end
end end
-- now poll my (global) status flags -- now poll my (global) status flags
@ -356,43 +507,46 @@ end
-- --
function reaper.installFullUIForCoa(coa) function reaper.installFullUIForCoa(coa)
-- install "Drone Control" as root for red and blue -- install "Drone Control" as root for red and blue
local mainMenu = nil local mainMenu = nil
if reaper.mainMenu then if reaper.mainMenu then
mainMenu = radioMenu.getMainMenuFor(reaper.mainMenu) -- nilling both next params will return menus[0] mainMenu = radioMenu.getMainMenuFor(reaper.mainMenu) -- nilling both next params will return menus[0]
end end
local root = missionCommands.addSubMenuForCoalition(coa, reaper.menuName, mainMenu) local root = missionCommands.addSubMenuForCoalition(coa, reaper.menuName, mainMenu)
-- now install submenus -- now install submenus
local c1 = missionCommands.addCommandForCoalition(coa, "Drone Status", root, reaper.redirectDroneStatus, {coa,}) reaper.installDCForCoa(coa, root)
local r2 = missionCommands.addSubMenuForCoalition(coa, "Launch Drones", root)
reaper.installLaunchersForCoa(coa, r2)
end end
function reaper.installLaunchersForCoa(coa, root) function reaper.installDCForCoa(coa, root)
-- WARNING: we currently install commands, may overflow!
-- trigger.action.outText("enter launchers builder", 30)
local filtered = {} local filtered = {}
for name, theZone in pairs(reaper.zones) do for name, theZone in pairs(reaper.zones) do
if theZone.coa == coa and theZone.launchUI then if theZone.coa == coa and (theZone.statusUI or theZone.launchUI) then
filtered[name] = theZone filtered[name] = theZone
end end
end end
local n = dcsCommon.getSizeOfTable(filtered) local n = dcsCommon.getSizeOfTable(filtered)
if n > 10 then if n > 10 then
trigger.action.outText("+++reap: WARNING too many (" .. n .. ") launchers for coa <" .. coa .. ">", 30) trigger.action.outText("+++reap: WARNING too many (" .. n .. ") drones for coa <" .. coa .. ">", 30)
return return
end end
for name, theZone in pairs(filtered) do for name, theZone in pairs(filtered) do
-- trigger.action.outText("proccing " .. name, 30) local mnu = theZone.name .. ": " .. theZone.myType
mnu = theZone.name .. ": " .. theZone.myType -- install menu for this drone
local r1 = missionCommands.addSubMenuForCoalition(coa, mnu, root)
-- install status and cycle target commands for this drone
local args = {coa, name, }
if theZone.launchUI then
mnu = "Launch " .. theZone.myType
if bank and reaper.useCost then if bank and reaper.useCost then
-- requires bank module -- requires bank module
mnu = mnu .. "" .. theZone.cost .. ")" mnu = mnu .. " " .. theZone.cost .. ")"
end end
local args = {coa, name, } local r3 = missionCommands.addCommandForCoalition(coa, mnu, r1, reaper.redirectLaunch, args)
local r3 = missionCommands.addCommandForCoalition(coa, mnu, root, reaper.redirectLaunch, args) end
if theZone.statusUI then
local r2 = missionCommands.addCommandForCoalition(coa, "Status Update", r1, reaper.redirectSingleStatus, args)
end
local r2 = missionCommands.addCommandForCoalition(coa, "Cycle target", r1, reaper.redirectCycleTarget, args)
end end
end end
@ -404,6 +558,13 @@ function reaper.redirectLaunch(args)
timer.scheduleFunction(reaper.doLaunch, args, timer.getTime() + 0.1) timer.scheduleFunction(reaper.doLaunch, args, timer.getTime() + 0.1)
end end
function reaper.redirectSingleStatus(args)
timer.scheduleFunction(reaper.doSingleStatusM, args, timer.getTime() + 0.1)
end
function reaper.redirectCycleTarget(args)
timer.scheduleFunction(reaper.doCylcleTarget, args, timer.getTime() + 0.1)
end
-- --
-- DML API for UI -- DML API for UI
-- --
@ -483,14 +644,12 @@ function reaper.doDroneStatus(args)
else else
msg = msg .. "\n\n(All drones have launched)\n" msg = msg .. "\n\n(All drones have launched)\n"
end end
trigger.action.outTextForCoalition(coa, msg, 30) trigger.action.outTextForCoalition(coa, msg, 30)
trigger.action.outSoundForCoalition(coa, reaper.actionSound) trigger.action.outSoundForCoalition(coa, reaper.actionSound)
end end
function reaper.doSingleDroneStatus(theZone) function reaper.doSingleDroneStatus(theZone)
local coa = theZone.coa local coa = theZone.coa
-- trigger.action.outText("enter SINGLE drone status for coa " .. coa, 30)
local msg = "" local msg = ""
local name = theZone.name local name = theZone.name
-- see if drone is tracking -- see if drone is tracking
@ -503,9 +662,31 @@ function reaper.doSingleDroneStatus(theZone)
lat, lon = dcsCommon.latLon2Text(lat, lon) lat, lon = dcsCommon.latLon2Text(lat, lon)
local ut = theTarget:getTypeName() local ut = theTarget:getTypeName()
msg = msg .. ut .. " at " .. lat .. ", " .. lon .. " code " .. theZone.code msg = msg .. ut .. " at " .. lat .. ", " .. lon .. " code " .. theZone.code
else
msg = msg .. "[signal failure, please try later]" -- now add full group intelligence
local collector = {}
local theGroup = theTarget:getGroup()
local allTargets = theGroup:getUnits()
for idx, aTgt in pairs(allTargets) do
if Unit.isExist(aTgt) then
local tn = aTgt:getTypeName()
if collector[tn] then collector[tn] = collector[tn] + 1
else collector[tn] = 1 end
end end
end
msg = msg .."\nGroup consists of: "
local i = 1
for name, count in pairs(collector) do
if i > 1 then msg = msg .. ", " end
msg = msg .. name
if count > 1 then msg = msg .. " (x" .. count .. ")" end
i = 2
end
msg = msg .. ".\n"
else
msg = msg .. "[signal failure, please try again later]"
end
trigger.action.outTextForCoalition(coa, msg, 30) trigger.action.outTextForCoalition(coa, msg, 30)
trigger.action.outSoundForCoalition(coa, reaper.actionSound) trigger.action.outSoundForCoalition(coa, reaper.actionSound)
return return
@ -528,9 +709,25 @@ function reaper.doSingleDroneStatus(theZone)
trigger.action.outSoundForCoalition(coa, reaper.actionSound) trigger.action.outSoundForCoalition(coa, reaper.actionSound)
end end
function reaper.doSingleStatusM(args)
local coa = args[1]
local name = args[2]
local theZone = reaper.zones[name]
if not theZone then end return
reaper.doSingleDroneStatus(theZone)
end
function reaper.doCylcleTarget(args)
local coa = args[1]
local name = args[2]
local theZone = reaper.zones[name]
if not theZone then end return
reaper.cycleTarget(theZone)
end
function reaper.doLaunch(args) function reaper.doLaunch(args)
coa = args[1] local coa = args[1]
name = args[2] local name = args[2]
-- check if we can launch -- check if we can launch
local theZone = reaper.zones[name] local theZone = reaper.zones[name]
if not theZone then if not theZone then
@ -581,7 +778,7 @@ function reaper.readConfigZone()
end end
reaper.name = "reaperConfig" -- zones comaptibility reaper.name = "reaperConfig" -- zones comaptibility
reaper.actionSound = theZone:getStringFromZoneProperty("actionSound", "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") reaper.actionSound = theZone:getStringFromZoneProperty("actionSound", "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav")
reaper.UI = theZone:getBoolFromZoneProperty("UI", true) reaper.hasUI = theZone:getBoolFromZoneProperty("UI", true)
reaper.menuName = theZone:getStringFromZoneProperty("menuName", "Drone Command") reaper.menuName = theZone:getStringFromZoneProperty("menuName", "Drone Command")
reaper.useCost = theZone:getBoolFromZoneProperty("useCost", true) reaper.useCost = theZone:getBoolFromZoneProperty("useCost", true)
if theZone:hasProperty("blueStatus?") then if theZone:hasProperty("blueStatus?") then
@ -668,7 +865,7 @@ function reaper.start()
end end
-- install UI if desired -- install UI if desired
if reaper.UI then if reaper.hasUI then
local coas = {1, 2} local coas = {1, 2}
for idx, coa in pairs(coas) do for idx, coa in pairs(coas) do
reaper.installFullUIForCoa(coa) reaper.installFullUIForCoa(coa)
@ -689,9 +886,9 @@ function reaper.start()
timer.scheduleFunction(reaper.update, {}, timer.getTime() + 1) timer.scheduleFunction(reaper.update, {}, timer.getTime() + 1)
-- schedule scan and track loops -- schedule scan and track loops
timer.scheduleFunction(reaper.scan, {}, timer.getTime() + 1) -- timer.scheduleFunction(reaper.scan, {}, timer.getTime() + 1)
timer.scheduleFunction(reaper.scanALT, {}, timer.getTime() + 1)
timer.scheduleFunction(reaper.track, {}, timer.getTime() + 1) timer.scheduleFunction(reaper.track, {}, timer.getTime() + 1)
trigger.action.outText("reaper v " .. reaper.version .. " running.", 30) trigger.action.outText("reaper v " .. reaper.version .. " running.", 30)
return true return true
end end
@ -702,7 +899,5 @@ end
--[[-- --[[--
Idea: mobile launch vehicle, zone follows apc around. Can even be hauled along with hook Idea: mobile launch vehicle, zone follows apc around. Can even be hauled along with hook
idea: prioritizing targets in a group
fix quad zone waypoints
filter targets for lasing by list?
--]]-- --]]--

View File

@ -1,5 +1,5 @@
cfxReconMode = {} cfxReconMode = {}
cfxReconMode.version = "2.2.1" cfxReconMode.version = "2.2.2"
cfxReconMode.verbose = false -- set to true for debug info 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.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered
@ -22,38 +22,6 @@ cfxReconMode.name = "cfxReconMode" -- to be compatible with test flags
--[[-- --[[--
VERSION HISTORY VERSION HISTORY
1.0.0 - initial version
1.0.1 - removeScoutByName()
1.0.2 - garbage collection
1.1.0 - autoRecon - any aircraft taking off immediately
signs up, no message when signing up or closing down
standalone - copied common procs lerp, agl, dist, distflat
from dcsCommon
report numbers
verbose flag
1.2.0 - queued recons. One scout per second for more even
performance
removed gc since it's now integrated into
update queue
removeScout optimization when directly passing name
playerOnlyRecon for autoRecon
red, blue, grey side filtering on auto scout
1.2.1 - parametrized report sound
1.3.0 - added black list, prio list functionality
1.3.1 - callbacks now also push name, as group can be dead
- removed bug when removing dead groups from map
1.4.0 - import dcsCommon, cfxZones etc
- added lib check
- config zone
- prio+
- detect+
1.4.1 - invocation no longer happen twice for prio.
- recon sound
- read all flight groups at start to get rid of the
- late activation work-around
1.5.0 - removeWhenDestroyed()
- autoRemove()
- readConfigZone creates default config zone so we get correct defaulting
2.0.0 - DML integration prio+-->prio! detect+ --> detect! 2.0.0 - DML integration prio+-->prio! detect+ --> detect!
and method and method
- changed access to prio and blacklist to hash - changed access to prio and blacklist to hash
@ -90,24 +58,9 @@ VERSION HISTORY
- new marksFadeAfter config attribute to control mark time - new marksFadeAfter config attribute to control mark time
- dmlZones OOP upgrade - dmlZones OOP upgrade
2.2.1 - fixed "cfxReconSMode" typo 2.2.1 - fixed "cfxReconSMode" typo
2.2.2 - added groupNames attribute
- clean-up
cfxReconMode is a script that allows units to perform reconnaissance
missions and, after detecting units, marks them on the map with
markers for their coalition and some text
Also, a callback is initiated for scouts as follows
signature: (reason, theSide, theSout, theGroup) with
reason a string
'detected' a group was detected
'removed' a mark for a group timed out
'priority' a member of prio group was detected
'start' a scout started scouting
'end' a scout stopped scouting
'dead' a scout has died and was removed from pool
theSide - side of the SCOUT that detected units
theScout - the scout that detected the group
theGroup - the group that is detected
theName - the group's name
--]]-- --]]--
cfxReconMode.detectionMinRange = 3000 -- meters at ground level cfxReconMode.detectionMinRange = 3000 -- meters at ground level
@ -137,9 +90,6 @@ cfxReconMode.marksFadeAfter = 30*60 -- after detection, marks disappear after
cfxReconMode.callbacks = {} -- sig: cb(reason, side, scout, group) cfxReconMode.callbacks = {} -- sig: cb(reason, side, scout, group)
cfxReconMode.uuidCount = 0 -- for unique marks cfxReconMode.uuidCount = 0 -- for unique marks
-- end standalone dcsCommon extract
function cfxReconMode.uuid() function cfxReconMode.uuid()
cfxReconMode.uuidCount = cfxReconMode.uuidCount + 1 cfxReconMode.uuidCount = cfxReconMode.uuidCount + 1
return cfxReconMode.uuidCount return cfxReconMode.uuidCount
@ -163,7 +113,6 @@ function cfxReconMode.addToPrioList(aGroup, dynamic)
aGroup = aGroup:getName() aGroup = aGroup:getName()
end end
if type(aGroup) == "string" then if type(aGroup) == "string" then
-- table.insert(cfxReconMode.prioList, aGroup)
cfxReconMode.prioList[aGroup] = aGroup cfxReconMode.prioList[aGroup] = aGroup
cfxReconMode.dynamics[aGroup] = dynamic cfxReconMode.dynamics[aGroup] = dynamic
end end
@ -176,7 +125,6 @@ function cfxReconMode.addToBlackList(aGroup, dynamic)
aGroup = aGroup:getName() aGroup = aGroup:getName()
end end
if type(aGroup) == "string" then if type(aGroup) == "string" then
--table.insert(cfxReconMode.blackList, aGroup)
cfxReconMode.blackList[aGroup] = aGroup cfxReconMode.blackList[aGroup] = aGroup
cfxReconMode.dynamics[aGroup] = dynamic cfxReconMode.dynamics[aGroup] = dynamic
end end
@ -230,15 +178,9 @@ function cfxReconMode.isStringInList(theString, theList)
end 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
-- are not allowed but you still want a unit from that side
-- to scout
-- since we use a queue for scouts, also always check the -- since we use a queue for scouts, also always check the
-- processed queue before adding to make sure a scout isn't -- processed queue before adding to make sure a scout isn't
-- entered multiple times -- entered multiple times
function cfxReconMode.addScout(theUnit) function cfxReconMode.addScout(theUnit)
if not theUnit then if not theUnit then
trigger.action.outText("+++cfxRecon: WARNING - nil Unit on add", 30) trigger.action.outText("+++cfxRecon: WARNING - nil Unit on add", 30)
@ -350,9 +292,9 @@ end
function cfxReconMode.placeMarkForUnit(location, theSide, theGroup) function cfxReconMode.placeMarkForUnit(location, theSide, theGroup)
local theID = cfxReconMode.uuid() local theID = cfxReconMode.uuid()
local theDesc = "Contact: "..theGroup:getName() local theDesc = "Contact"
if cfxReconMode.groupNames then theDesc = theDesc .. ": " ..theGroup:getName() end
if cfxReconMode.reportNumbers then if cfxReconMode.reportNumbers then
-- theDesc = theDesc .. " (" .. theGroup:getSize() .. " units)"
theDesc = theDesc .. " - " .. cfxReconMode.getSit(theGroup) .. ", " .. cfxReconMode.getAction(theGroup) .. "." theDesc = theDesc .. " - " .. cfxReconMode.getSit(theGroup) .. ", " .. cfxReconMode.getAction(theGroup) .. "."
end end
trigger.action.markToCoalition( trigger.action.markToCoalition(
@ -466,7 +408,9 @@ function cfxReconMode.getTimeData()
end end
function cfxReconMode.generateSALT(theScout, theGroup) function cfxReconMode.generateSALT(theScout, theGroup)
local msg = theScout:getName() .. " reports new ground contact " .. theGroup:getName() .. ":\n" 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 -- 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.getSit(theGroup) .. ", "-- S
msg = msg .. cfxReconMode.getAction(theGroup) .. ", " -- A msg = msg .. cfxReconMode.getAction(theGroup) .. ", " -- A
@ -559,7 +503,6 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc)
-- see if it was a prio target -- see if it was a prio target
if inList then if inList then
-- if cfxReconMode.announcer then
if cfxReconMode.verbose then if cfxReconMode.verbose then
trigger.action.outText("+++rcn: Priority target spotted", 30) trigger.action.outText("+++rcn: Priority target spotted", 30)
end end
@ -1049,7 +992,7 @@ function cfxReconMode.readConfigZone()
if theZone:hasProperty("imperialUnits") then if theZone:hasProperty("imperialUnits") then
cfxReconMode.imperialUnits = theZone:getBoolFromZoneProperty( "imperialUnits", false) cfxReconMode.imperialUnits = theZone:getBoolFromZoneProperty( "imperialUnits", false)
end end
cfxReconMode.groupNames = theZone:getBoolFromZoneProperty( "groupNames", true)
cfxReconMode.theZone = theZone -- save this zone cfxReconMode.theZone = theZone -- save this zone
end end
@ -1200,9 +1143,6 @@ if not cfxReconMode.start() then
cfxReconMode = nil cfxReconMode = nil
end end
-- debug: wire up my own callback
-- cfxReconMode.addCallback(cfxReconMode.demoReconCB)
--[[-- --[[--

View File

@ -1,11 +1,16 @@
sweeper = {} sweeper = {}
sweeper.version = "1.0.0" sweeper.version = "1.0.1"
sweeper.requiredLibs = { sweeper.requiredLibs = {
"dcsCommon", "dcsCommon",
"cfxZones", "cfxZones",
} }
-- remove all units that are detected twice in a row in the same -- remove all units that are detected twice in a row in the same
-- zone after a time interval. Used to remove deadlocked units. -- zone after a time interval. Used to remove deadlocked units.
--[[--
VERSION HISTORY
1.0.1 - Initial version
--]]--
sweeper.zones = {} sweeper.zones = {}
sweeper.interval = 5 * 60 -- 5 mins (max 10 mins) in zone will kill you sweeper.interval = 5 * 60 -- 5 mins (max 10 mins) in zone will kill you
@ -18,7 +23,7 @@ end
function sweeper.readSweeperZone(theZone) function sweeper.readSweeperZone(theZone)
theZone.aircraft = theZone:getBoolFromZoneProperty("aircraft", true) theZone.aircraft = theZone:getBoolFromZoneProperty("aircraft", true)
theZone.helos = theZone:getBoolFromZoneProperty("helos", false) theZone.helos = theZone:getBoolFromZoneProperty("helos", true)
end end
function sweeper.update() function sweeper.update()
@ -102,6 +107,16 @@ function sweeper.update()
sweeper.flights = newFlights sweeper.flights = newFlights
end end
function sweeper.readConfig()
local theZone = cfxZones.getZoneByName("sweeperConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("sweeperConfig")
end
sweeper.name = "sweeperConfig" -- zones comaptibility
sweeper.interval = theZone:getNumberFromZoneProperty("interval", 5 * 60)
sewwper.verbose = theZone.verbose
end
function sweeper.start() function sweeper.start()
-- lib check -- lib check
if not dcsCommon.libCheck then if not dcsCommon.libCheck then
@ -112,6 +127,8 @@ function sweeper.start()
return false return false
end end
sweeper.readConfig()
-- process sweeper Zones -- process sweeper Zones
local attrZones = cfxZones.getZonesWithAttributeNamed("sweeper") local attrZones = cfxZones.getZonesWithAttributeNamed("sweeper")
for k, aZone in pairs(attrZones) do for k, aZone in pairs(attrZones) do
@ -119,7 +136,7 @@ function sweeper.start()
sweeper.addSweeperZone(aZone) -- add to list sweeper.addSweeperZone(aZone) -- add to list
end end
-- start update in 5 seconds -- start update in (interval)
timer.scheduleFunction(sweeper.update, {}, timer.getTime() + sweeper.interval) timer.scheduleFunction(sweeper.update, {}, timer.getTime() + sweeper.interval)
-- say hi -- say hi

View File

@ -1,5 +1,5 @@
williePete = {} williePete = {}
williePete.version = "2.0.3" williePete.version = "2.0.4"
williePete.ups = 10 -- we update at 10 fps, so accuracy of a williePete.ups = 10 -- we update at 10 fps, so accuracy of a
-- missile moving at Mach 2 is within 33 meters, -- missile moving at Mach 2 is within 33 meters,
-- with interpolation even at 3 meters -- with interpolation even at 3 meters
@ -20,6 +20,7 @@ williePete.requiredLibs = {
2.0.1 - added Harrier's FFAR M156 WP 2.0.1 - added Harrier's FFAR M156 WP
2.0.2 - hardened playerUpdate() 2.0.2 - hardened playerUpdate()
2.0.3 - further hardened playerUpdate() 2.0.3 - further hardened playerUpdate()
2.0.4 - support for the Kiowa's Hydra M259
--]]-- --]]--
williePete.willies = {} williePete.willies = {}
@ -31,7 +32,8 @@ williePete.blastedObjects = {} -- used when we detonate something
-- recognizes WP munitions. May require regular update when new -- recognizes WP munitions. May require regular update when new
-- models come out. -- models come out.
williePete.smokeWeapons = {"HYDRA_70_M274","HYDRA_70_MK61","HYDRA_70_MK1","HYDRA_70_WTU1B","HYDRA_70_M156","HYDRA_70_M158","BDU_45B","BDU_33","BDU_45","BDU_45LGB","BDU_50HD","BDU_50LD","BDU_50LGB","C_8CM", "SNEB_TYPE254_H1_GREEN", "SNEB_TYPE254_H1_RED", "SNEB_TYPE254_H1_YELLOW", "FFAR M156 WP"} williePete.smokeWeapons = {"HYDRA_70_M274","HYDRA_70_MK61","HYDRA_70_MK1","HYDRA_70_WTU1B","HYDRA_70_M156","HYDRA_70_M158","BDU_45B","BDU_33","BDU_45","BDU_45LGB","BDU_50HD","BDU_50LD","BDU_50LGB","C_8CM", "SNEB_TYPE254_H1_GREEN", "SNEB_TYPE254_H1_RED", "SNEB_TYPE254_H1_YELLOW", "FFAR M156 WP",
"HYDRA_70_M259"}
function williePete.addWillie(theWillie) function williePete.addWillie(theWillie)
table.insert(williePete.willies, theWillie) table.insert(williePete.willies, theWillie)