Version 1.1.4

sequencer
messenger
radioMenu
This commit is contained in:
Christian Franz 2022-09-01 13:06:51 +02:00
parent 4831f7597f
commit 58d81e162f
17 changed files with 1253 additions and 111 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
rndFlags = {} rndFlags = {}
rndFlags.version = "1.4.0" rndFlags.version = "1.4.1"
rndFlags.verbose = false rndFlags.verbose = false
rndFlags.requiredLibs = { rndFlags.requiredLibs = {
"dcsCommon", -- always "dcsCommon", -- always
@ -32,6 +32,7 @@ rndFlags.requiredLibs = {
1.3.2 - moved flagArrayFromString to dcsCommon 1.3.2 - moved flagArrayFromString to dcsCommon
- minor clean-up - minor clean-up
1.4.0 - persistence 1.4.0 - persistence
1.4.1 - a little less verbosity
--]] --]]
@ -336,8 +337,10 @@ function rndFlags.start()
-- process RND Zones -- process RND Zones
local attrZones = cfxZones.getZonesWithAttributeNamed("RND!") local attrZones = cfxZones.getZonesWithAttributeNamed("RND!")
a = dcsCommon.getSizeOfTable(attrZones) if rndFlags.verbose then
trigger.action.outText("RND! zones: " .. a, 30) local a = dcsCommon.getSizeOfTable(attrZones)
trigger.action.outText("RND! zones: " .. a, 30)
end
-- now create an rnd gen for each one and add them -- now create an rnd gen for each one and add them
-- to our watchlist -- to our watchlist

View File

@ -1,5 +1,5 @@
cfxMX = {} cfxMX = {}
cfxMX.version = "1.2.2" cfxMX.version = "1.2.3"
cfxMX.verbose = false cfxMX.verbose = false
--[[-- --[[--
Mission data decoder. Access to ME-built mission structures Mission data decoder. Access to ME-built mission structures
@ -21,10 +21,15 @@ cfxMX.verbose = false
1.2.2 - fixed ctry bug in countryByName 1.2.2 - fixed ctry bug in countryByName
- playerGroupByName - playerGroupByName
- playerUnitByName - playerUnitByName
1.2.3 - groupTypeByName
- groupCoalitionByName
--]]-- --]]--
cfxMX.groupNamesByID = {} cfxMX.groupNamesByID = {}
cfxMX.groupIDbyName = {} cfxMX.groupIDbyName = {}
cfxMX.groupDataByName = {} cfxMX.groupDataByName = {}
cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"...
cfxMX.groupCoalitionByName = {}
cfxMX.countryByName ={} cfxMX.countryByName ={}
cfxMX.linkByName = {} cfxMX.linkByName = {}
cfxMX.allFixedByName = {} cfxMX.allFixedByName = {}
@ -205,11 +210,12 @@ function cfxMX.createCrossReferences()
linkUnit = group_data.route.points[1].linkUnit linkUnit = group_data.route.points[1].linkUnit
cfxMX.linkByName[aName] = linkUnit cfxMX.linkByName[aName] = linkUnit
end end
cfxMX.groupTypeByName[aName] = category
cfxMX.groupNamesByID[aID] = aName cfxMX.groupNamesByID[aID] = aName
cfxMX.groupIDbyName[aName] = aID cfxMX.groupIDbyName[aName] = aID
cfxMX.groupDataByName[aName] = group_data cfxMX.groupDataByName[aName] = group_data
cfxMX.countryByName[aName] = countryID -- !!! was cntry_id cfxMX.countryByName[aName] = countryID -- !!! was cntry_id
cfxMX.groupCoalitionByName[aName] = coaNum
-- now make the type-specific xrefs -- now make the type-specific xrefs
if obj_type_name == "helicopter" then if obj_type_name == "helicopter" then

View File

@ -1,5 +1,5 @@
cfxReconMode = {} cfxReconMode = {}
cfxReconMode.version = "2.1.1" cfxReconMode.version = "2.1.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
@ -79,6 +79,9 @@ VERSION HISTORY
- activate / deactivate by flags - activate / deactivate by flags
2.1.1 - Lat Lon and MGRS also give Elevation 2.1.1 - Lat Lon and MGRS also give Elevation
- cfxReconMode.reportTime - cfxReconMode.reportTime
2.1.2 - imperialUnits for elevation
- <ele> wildcard in message format
- fix for mgrs bug in message (zone coords, not unit)
cfxReconMode is a script that allows units to perform reconnaissance cfxReconMode is a script that allows units to perform reconnaissance
missions and, after detecting units, marks them on the map with missions and, after detecting units, marks them on the map with
@ -424,13 +427,21 @@ function cfxReconMode.getLocation(theGroup)
local theUnit = theGroup:getUnit(1) local theUnit = theGroup:getUnit(1)
local currPoint = theUnit:getPoint() local currPoint = theUnit:getPoint()
local ele = math.floor(land.getHeight({x = currPoint.x, y = currPoint.z})) local ele = math.floor(land.getHeight({x = currPoint.x, y = currPoint.z}))
local units = "m"
if cfxReconMode.imperialUnits then
ele = math.floor(ele * 3.28084) -- feet
units = "ft"
else
ele = math.floor(ele) -- meters
end
if cfxReconMode.mgrs then if cfxReconMode.mgrs then
local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint))
msg = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing .. " Ele " .. ele .."m" msg = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing .. " Ele " .. ele .. units
else else
local lat, lon, alt = coord.LOtoLL(currPoint) local lat, lon, alt = coord.LOtoLL(currPoint)
lat, lon = dcsCommon.latLon2Text(lat, lon) lat, lon = dcsCommon.latLon2Text(lat, lon)
msg = "Lat " .. lat .. " Lon " .. lon .. " Ele " .. ele .."m" msg = "Lat " .. lat .. " Lon " .. lon .. " Ele " .. ele ..units
end end
return msg return msg
end end
@ -483,12 +494,21 @@ function cfxReconMode.processZoneMessage(inMsg, theZone, theGroup)
local theUnit = dcsCommon.getFirstLivingUnit(theGroup) local theUnit = dcsCommon.getFirstLivingUnit(theGroup)
currPoint = theUnit:getPoint() currPoint = theUnit:getPoint()
end end
local ele = math.floor(land.getHeight({x = currPoint.x, y = currPoint.z}))
local units = "m"
if cfxReconMode.imperialUnits then
ele = math.floor(ele * 3.28084) -- feet
units = "ft"
else
ele = math.floor(ele) -- meters
end
local lat, lon, alt = coord.LOtoLL(currPoint) local lat, lon, alt = coord.LOtoLL(currPoint)
lat, lon = dcsCommon.latLon2Text(lat, lon) lat, lon = dcsCommon.latLon2Text(lat, lon)
outMsg = outMsg:gsub("<lat>", lat) outMsg = outMsg:gsub("<lat>", lat)
outMsg = outMsg:gsub("<lon>", lon) outMsg = outMsg:gsub("<lon>", lon)
currPoint = cfxZones.getPoint(theZone) outMsg = outMsg:gsub("<ele>", ele..units)
--currPoint = cfxZones.getPoint(theZone)
local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint))
local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
outMsg = outMsg:gsub("<mgrs>", mgrs) outMsg = outMsg:gsub("<mgrs>", mgrs)
@ -1019,6 +1039,10 @@ function cfxReconMode.readConfigZone()
cfxReconMode.lastDeActivate = cfxZones.getFlagValue(cfxReconMode.deactivate, theZone) cfxReconMode.lastDeActivate = cfxZones.getFlagValue(cfxReconMode.deactivate, theZone)
end end
cfxReconMode.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperial", false)
if cfxZones.hasProperty(theZone, "imperialUnits") then
cfxReconMode.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperialUnits", false)
end
cfxReconMode.theZone = theZone -- save this zone cfxReconMode.theZone = theZone -- save this zone
end end

View File

@ -1,5 +1,5 @@
cloneZones = {} cloneZones = {}
cloneZones.version = "1.5.2" cloneZones.version = "1.5.4"
cloneZones.verbose = false cloneZones.verbose = false
cloneZones.requiredLibs = { cloneZones.requiredLibs = {
"dcsCommon", -- always "dcsCommon", -- always
@ -59,6 +59,8 @@ cloneZones.allCObjects = {} -- all clones objects
1.5.0 - persistence 1.5.0 - persistence
1.5.1 - fixed static data cloning bug (load & save) 1.5.1 - fixed static data cloning bug (load & save)
1.5.2 - fixed bug in trackWith: referencing wrong cloner 1.5.2 - fixed bug in trackWith: referencing wrong cloner
1.5.3 - centerOnly/wholeGroups attribute for rndLoc, rndHeading and onRoad
1.5.4 - parking for aircraft processing when cloning from template
@ -148,7 +150,7 @@ function cloneZones.allGroupsInZoneByData(theZone)
end end
function cloneZones.createClonerWithZone(theZone) -- has "Cloner" function cloneZones.createClonerWithZone(theZone) -- has "Cloner"
if cloneZones.verbose then if cloneZones.verbose or theZone.verbose then
trigger.action.outText("+++clnZ: new cloner " .. theZone.name, 30) trigger.action.outText("+++clnZ: new cloner " .. theZone.name, 30)
end end
@ -292,6 +294,11 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner"
if cfxZones.hasProperty(theZone, "rndLoc") then if cfxZones.hasProperty(theZone, "rndLoc") then
theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "rndLoc", false) theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "rndLoc", false)
end end
theZone.centerOnly = cfxZones.getBoolFromZoneProperty(theZone, "centerOnly", false)
if cfxZones.hasProperty(theZone, "wholeGroups") then
theZone.centerOnly = cfxZones.getBoolFromZoneProperty(theZone, "wholeGroups", false)
end
theZone.rndHeading = cfxZones.getBoolFromZoneProperty(theZone, "rndHeading", false) theZone.rndHeading = cfxZones.getBoolFromZoneProperty(theZone, "rndHeading", false)
theZone.onRoad = cfxZones.getBoolFromZoneProperty(theZone, "onRoad", false) theZone.onRoad = cfxZones.getBoolFromZoneProperty(theZone, "onRoad", false)
@ -308,13 +315,16 @@ end
-- --
function cloneZones.despawnAll(theZone) function cloneZones.despawnAll(theZone)
if cloneZones.verbose then if cloneZones.verbose or theZone.verbose then
trigger.action.outText("wiping <" .. theZone.name .. ">", 30) trigger.action.outText("+++clnZ: despawn all - wiping zone <" .. theZone.name .. ">", 30)
end end
for idx, aGroup in pairs(theZone.mySpawns) do for idx, aGroup in pairs(theZone.mySpawns) do
--trigger.action.outText("++clnZ: despawn all " .. aGroup.name, 30) --trigger.action.outText("++clnZ: despawn all " .. aGroup.name, 30)
if aGroup:isExist() then if aGroup:isExist() then
if theZone.verbose then
trigger.action.outText("+++clnZ: will destroy <" .. aGroup:getName() .. ">", 30)
end
cloneZones.invokeCallbacks(theZone, "will despawn group", aGroup) cloneZones.invokeCallbacks(theZone, "will despawn group", aGroup)
Group.destroy(aGroup) Group.destroy(aGroup)
end end
@ -323,7 +333,7 @@ function cloneZones.despawnAll(theZone)
-- warning! may be mismatch because we are looking at groups -- warning! may be mismatch because we are looking at groups
-- not objects. let's see -- not objects. let's see
if aStatic:isExist() then if aStatic:isExist() then
if cloneZones.verbose then if cloneZones.verbose or theZone.verbose then
trigger.action.outText("Destroying static <" .. aStatic:getName() .. ">", 30) trigger.action.outText("Destroying static <" .. aStatic:getName() .. ">", 30)
end end
cloneZones.invokeCallbacks(theZone, "will despawn static", aStatic) cloneZones.invokeCallbacks(theZone, "will despawn static", aStatic)
@ -334,12 +344,47 @@ function cloneZones.despawnAll(theZone)
theZone.myStatics = {} theZone.myStatics = {}
end end
function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWaypoints) function cloneZones.assignClosestParking(theData)
-- on enter: theData has units with updated x, y
-- and waypoint 1 action is From Parking
-- and it has at least one unit
-- let's get the airbase
local theRoute = theData.route -- we know it exists
local thePoints = theRoute.points
local firstPoint = thePoints[1]
local loc = {}
loc.x = firstPoint.x
loc.y = 0
loc.z = firstPoint.y
local theAirbase = dcsCommon.getClosestAirbaseTo(loc)
-- now let's assign free slots closest to unit
local slotsTaken = {}
local units = theData.units
local cat = cfxMX.groupTypeByName[theData.name]
for idx, theUnit in pairs(units) do
local newSlot = dcsCommon.getClosestFreeSlotForCatInAirbaseTo(cat, theUnit.x, theUnit.y, theAirbase, slotsTaken)
if newSlot then
local slotNo = newSlot.Term_Index
--trigger.action.outText("unit <" .. theUnit.name .. "> old slot <" .. theUnit.parking .. "> to new slot <" .. slotNo .. ">", 30)
theUnit.parking_id = nil -- !! or you b screwed
theUnit.parking = slotNo -- !! screw parking_ID, they don't match
theUnit.x = newSlot.vTerminalPos.x
theUnit.y = newSlot.vTerminalPos.z -- !!!
table.insert(slotsTaken, slotNo)
end
end
end
function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWaypoints)
-- enter with theData being group's data block
-- remember that zoneDelta's [z] modifies theData's y!! -- remember that zoneDelta's [z] modifies theData's y!!
theData.x = theData.x + zoneDelta.x theData.x = theData.x + zoneDelta.x
theData.y = theData.y + zoneDelta.z -- !!! theData.y = theData.y + zoneDelta.z -- !!!
local units = theData.units local units = theData.units
local departFromAerodrome = false
--local departingAerodrome
local fromParking = false
for idx, aUnit in pairs(units) do for idx, aUnit in pairs(units) do
aUnit.x = aUnit.x + zoneDelta.x aUnit.x = aUnit.x + zoneDelta.x
aUnit.y = aUnit.y + zoneDelta.z -- again!!!! aUnit.y = aUnit.y + zoneDelta.z -- again!!!!
@ -373,8 +418,11 @@ function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWayp
loc.y = 0 loc.y = 0
loc.z = firstPoint.y loc.z = firstPoint.y
local bestAirbase = dcsCommon.getClosestAirbaseTo(loc) local bestAirbase = dcsCommon.getClosestAirbaseTo(loc)
--departingAerodrome = bestAirbase
firstPoint.airdromeId = bestAirbase:getID() firstPoint.airdromeId = bestAirbase:getID()
-- trigger.action.outText("first: adjusted to " .. firstPoint.airdromeId, 30) -- trigger.action.outText("first: adjusted to " .. firstPoint.airdromeId, 30)
departFromAerodrome = true
fromParking = dcsCommon.stringStartsWith(firstPoint.action, "From Parking")
end end
-- adjust last point (landing) -- adjust last point (landing)
@ -393,8 +441,20 @@ function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWayp
end end
end end
end -- if theRoute
-- now process departing slot if given
if departFromAerodrome then
-- we may need alt from land to add here, maybe later
-- now process parking slots, and choose closest slot
-- per unit's location
if fromParking then
cloneZones.assignClosestParking(theData)
end
end end
end end
function cloneZones.uniqueID() function cloneZones.uniqueID()
local uid = cloneZones.uniqueCounter local uid = cloneZones.uniqueCounter
cloneZones.uniqueCounter = cloneZones.uniqueCounter + 1 cloneZones.uniqueCounter = cloneZones.uniqueCounter + 1
@ -670,6 +730,9 @@ function cloneZones.handoffTracking(theGroup, theZone)
end end
function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
if cloneZones.verbose or spawnZone.verbose then
trigger.action.outText("+++clnZ: spawning with template <" .. theZone.name .. "> for spawner <" .. spawnZone.name .. ">", 30)
end
-- theZone is the cloner with the template -- theZone is the cloner with the template
-- spawnZone is the spawner with settings -- spawnZone is the spawner with settings
-- if not spawnZone then spawnZone = theZone end -- if not spawnZone then spawnZone = theZone end
@ -697,17 +760,34 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
local theCat = cfxMX.catText2ID(cat) local theCat = cfxMX.catText2ID(cat)
rawData.CZtheCat = theCat -- save cat rawData.CZtheCat = theCat -- save cat
-- update their position if not spawning to exact same location -- update their position if not spawning to exact same location
if cloneZones.verbose or theZone.verbose then
trigger.action.outText("+++clnZ: tmpl delta x = <" .. math.floor(zoneDelta.x) .. ">, y = <" .. math.floor(zoneDelta.z) .. "> for tmpl <" .. theZone.name .. "> to cloner <" .. spawnZone.name .. ">", 30)
end
cloneZones.updateLocationsInGroupData(rawData, zoneDelta, spawnZone.moveRoute) cloneZones.updateLocationsInGroupData(rawData, zoneDelta, spawnZone.moveRoute)
-- apply randomizer if selected -- apply randomizer if selected
if spawnZone.rndLoc then if spawnZone.rndLoc then
--trigger.action.outText("rndloc for <" .. spawnZone.name .. ">", 30)
-- calculate the entire group's displacement
local units = rawData.units local units = rawData.units
local r = math.random() * spawnZone.radius
local phi = 6.2831 * math.random() -- that's 2Pi, folx
local dx = r * math.cos(phi)
local dy = r * math.sin(phi)
for idx, aUnit in pairs(units) do for idx, aUnit in pairs(units) do
local r = math.random() * spawnZone.radius if not spawnZone.centerOnly then
local phi = 6.2831 * math.random() -- that's 2Pi, folx -- *every unit's displacement is randomized
local dx = r * math.cos(phi) r = math.random() * spawnZone.radius
local dy = r * math.sin(phi) phi = 6.2831 * math.random() -- that's 2Pi, folx
dx = r * math.cos(phi)
dy = r * math.sin(phi)
end
if spawnZone.verbose or cloneZones.verbose then
trigger.action.outText("+++clnZ: <" .. spawnZone.name .. "> R = " .. spawnZone.radius .. ":G<" .. rawData.name .. "/" .. aUnit.name .. "> - rndLoc: r = " .. r .. ", dx = " .. dx .. ", dy= " .. dy .. ".", 30)
end
aUnit.x = aUnit.x + dx aUnit.x = aUnit.x + dx
aUnit.y = aUnit.y + dy aUnit.y = aUnit.y + dy
end end
@ -715,44 +795,71 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
if spawnZone.rndHeading then if spawnZone.rndHeading then
local units = rawData.units local units = rawData.units
for idx, aUnit in pairs(units) do if spawnZone.centerOnly and units and units[1] then
local phi = 6.2831 * math.random() -- that's 2Pi, folx -- rotate entire group around unit 1
aUnit.heading = phi local cx = units[1].x
local cy = units[1].y
local degrees = 360 * math.random() -- rotateGroupData uses degrees
dcsCommon.rotateGroupData(rawData, degrees, cx, cy)
else
for idx, aUnit in pairs(units) do
local phi = 6.2831 * math.random() -- that's 2Pi, folx
aUnit.heading = phi
end
end end
end end
-- apply onRoad option if selected -- apply onRoad option if selected
if spawnZone.onRoad then if spawnZone.onRoad then
local units = rawData.units local units = rawData.units
local iterCount = 0 if spawnZone.centerOnly then
local otherLocs = {} -- resolved locs -- only place the first unit in group on roads
for idx, aUnit in pairs(units) do -- and displace all other with the same offset
local cx = aUnit.x local hasOffset = false
local cy = aUnit.y local dx, dy, cx, cy
-- we now iterate until there is enough separation or too many iters for idx, aUnit in pairs(units) do
local tooClose cx = aUnit.x
local np, nx, ny cy = aUnit.y
repeat if not hasOffset then
nx, ny = land.getClosestPointOnRoads("roads", cx, cy) local nx, ny = land.getClosestPointOnRoads("roads", cx, cy)
-- compare this with all other locs dx = nx - cx
np = {x=nx, y=ny} dy = ny - cy
tooClose = false hasOffset = true
for idc, op in pairs(otherLocs) do end
local d = dcsCommon.dist(np, op) aUnit.x = cx + dx
if d < cloneZones.minSep then aUnit.y = cy + dy
tooClose = true end
cx = cx + cloneZones.minSep else
cy = cy + cloneZones.minSep local iterCount = 0
iterCount = iterCount + 1 local otherLocs = {} -- resolved locs
-- trigger.action.outText("d fail for <" .. aUnit.name.. ">: d= <" .. d .. ">, iters = <" .. iterCount .. ">", 30) for idx, aUnit in pairs(units) do
end local cx = aUnit.x
end local cy = aUnit.y
until (iterCount > cloneZones.maxIter) or (not tooClose) -- we now iterate until there is enough separation or too many iters
-- trigger.action.outText("separation iters for <" .. aUnit.name.. ">:<" .. iterCount .. ">", 30) local tooClose
table.insert(otherLocs, np) local np, nx, ny
aUnit.x = nx repeat
aUnit.y = ny nx, ny = land.getClosestPointOnRoads("roads", cx, cy)
end -- compare this with all other locs
np = {x=nx, y=ny}
tooClose = false
for idc, op in pairs(otherLocs) do
local d = dcsCommon.dist(np, op)
if d < cloneZones.minSep then
tooClose = true
cx = cx + cloneZones.minSep
cy = cy + cloneZones.minSep
iterCount = iterCount + 1
-- trigger.action.outText("d fail for <" .. aUnit.name.. ">: d= <" .. d .. ">, iters = <" .. iterCount .. ">", 30)
end
end
until (iterCount > cloneZones.maxIter) or (not tooClose)
-- trigger.action.outText("separation iters for <" .. aUnit.name.. ">:<" .. iterCount .. ">", 30)
table.insert(otherLocs, np)
aUnit.x = nx
aUnit.y = ny
end
end -- else centerOnly
end end
@ -946,6 +1053,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
end end
function cloneZones.spawnWithCloner(theZone) function cloneZones.spawnWithCloner(theZone)
trigger.action.outText("+++clnZ: enter spawnWithCloner for <" .. theZone.name .. ">", 30)
if not theZone then if not theZone then
trigger.action.outText("+++clnZ: nil zone on spawnWithCloner", 30) trigger.action.outText("+++clnZ: nil zone on spawnWithCloner", 30)
return return
@ -966,14 +1074,17 @@ function cloneZones.spawnWithCloner(theZone)
local templates = dcsCommon.splitString(templateName, ",") local templates = dcsCommon.splitString(templateName, ",")
templateName = dcsCommon.pickRandom(templates) templateName = dcsCommon.pickRandom(templates)
templateName = dcsCommon.trim(templateName) templateName = dcsCommon.trim(templateName)
if cloneZones.verbose then if cloneZones.verbose or theZone.verbose then
trigger.action.outText("+++clnZ: picked random template <" .. templateName .."> for from <" .. allNames .. "> for cloner " .. theZone.name, 30) trigger.action.outText("+++clnZ: picked random template <" .. templateName .."> for from <" .. allNames .. "> for cloner " .. theZone.name, 30)
end end
end end
if cloneZones.verbose or theZone.verbose then
trigger.action.outText("+++clnZ: spawning - picked <" .. templateName .. "> as template", 30)
end
local newTemplate = cloneZones.getCloneZoneByName(templateName) local newTemplate = cloneZones.getCloneZoneByName(templateName)
if not newTemplate then if not newTemplate then
if cloneZones.verbose then if cloneZones.verbose or theZone.verbose then
trigger.action.outText("+++clnZ: no clone source with name <" .. templateName .."> for cloner " .. theZone.name, 30) trigger.action.outText("+++clnZ: no clone source with name <" .. templateName .."> for cloner " .. theZone.name, 30)
end end
return return
@ -1118,10 +1229,10 @@ function cloneZones.update()
end end
end end
function cloneZones.onStart() function cloneZones.doOnStart()
--trigger.action.outText("+++clnZ: Enter atStart", 30)
for idx, theZone in pairs(cloneZones.cloners) do for idx, theZone in pairs(cloneZones.cloners) do
if theZone.onStart then if theZone.onStart then
trigger.action.outText("+++clnZ: onStart true for <" .. theZone.name .. ">", 30)
if theZone.isStarted then if theZone.isStarted then
if cloneZones.verbose or theZone.verbose then if cloneZones.verbose or theZone.verbose then
trigger.action.outText("+++clnz: onStart pre-empted for <" .. theZone.name .. "> by persistence", 30) trigger.action.outText("+++clnz: onStart pre-empted for <" .. theZone.name .. "> by persistence", 30)
@ -1422,7 +1533,7 @@ function cloneZones.start()
-- cycles to go through object removal -- cycles to go through object removal
-- persistencey has loaded isStarted if a cloner was -- persistencey has loaded isStarted if a cloner was
-- already started -- already started
timer.scheduleFunction(cloneZones.onStart, {}, timer.getTime() + 0.1) timer.scheduleFunction(cloneZones.doOnStart, {}, timer.getTime() + 1.0)
-- start update -- start update
cloneZones.update() cloneZones.update()

View File

@ -1,5 +1,5 @@
dcsCommon = {} dcsCommon = {}
dcsCommon.version = "2.7.1" dcsCommon.version = "2.7.2"
--[[-- VERSION HISTORY --[[-- VERSION HISTORY
2.2.6 - compassPositionOfARelativeToB 2.2.6 - compassPositionOfARelativeToB
- clockPositionOfARelativeToB - clockPositionOfARelativeToB
@ -52,7 +52,7 @@ dcsCommon.version = "2.7.1"
- dcsCommon.trimArray( - dcsCommon.trimArray(
- createStaticObjectData uses trim for type - createStaticObjectData uses trim for type
- getEnemyCoalitionFor understands strings, still returns number - getEnemyCoalitionFor understands strings, still returns number
- coalition2county also undertsands 'red' and 'blue' - coalition2county also understands 'red' and 'blue'
2.5.0 - "Line" formation with one unit places unit at center 2.5.0 - "Line" formation with one unit places unit at center
2.5.1 - vNorm(a) 2.5.1 - vNorm(a)
2.5.1 - added SA-18 Igla manpad to unitIsInfantry() 2.5.1 - added SA-18 Igla manpad to unitIsInfantry()
@ -92,6 +92,12 @@ dcsCommon.version = "2.7.1"
2.7.1 - new isPlayerUnit() -- moved from cfxPlayer 2.7.1 - new isPlayerUnit() -- moved from cfxPlayer
new getAllExistingPlayerUnitsRaw - from cfxPlayer new getAllExistingPlayerUnitsRaw - from cfxPlayer
new typeIsInfantry() new typeIsInfantry()
2.7.2 - new rangeArrayFromString()
fixed leading blank bug in flagArrayFromString
new incFlag()
new decFlag()
nil trap in stringStartsWith()
new getClosestFreeSlotForCatInAirbaseTo()
--]]-- --]]--
@ -388,6 +394,64 @@ dcsCommon.version = "2.7.1"
return closestBase, delta return closestBase, delta
end end
function dcsCommon.getClosestFreeSlotForCatInAirbaseTo(cat, x, y, theAirbase, ignore)
if not theAirbase then return nil end
if not ignore then ignore = {} end
if not cat then return nil end
if (not cat == "helicopter") and (not cat == "plane") then
trigger.action.outText("+++common-getslotforcat: wrong cat <" .. cat .. ">", 30)
return nil
end
local allFree = theAirbase:getParking(true) -- only free slots
local filterFreeByType = {}
for idx, aSlot in pairs(allFree) do
local termT = aSlot.Term_Type
if termT == 104 or
(termT == 72 and cat == "plane") or
(termT == 68 and cat == "plane") or
(termT == 40 and cat == "helicopter") then
table.insert(filterFreeByType, aSlot)
else
-- we skip this slot, not good for type
end
end
if #filterFreeByType == 0 then
return nil
end
local reallyFree = {}
for idx, aSlot in pairs(filterFreeByType) do
local slotNum = aSlot.Term_Index
isTaken = false
for idy, taken in pairs(ignore) do
if taken == slotNum then isTaken = true end
end
if not isTaken then
table.insert(reallyFree, aSlot)
end
end
if #reallyFree < 1 then
reallyFree = filterFreeByType
end
local closestDist = math.huge
local closestSlot = nil
local p = {x = x, y = 0, z = y} -- !!
for idx, aSlot in pairs(reallyFree) do
local sp = {x = aSlot.vTerminalPos.x, y = 0, z = aSlot.vTerminalPos.z}
local currDist = dcsCommon.distFlat(p, sp)
--trigger.action.outText("slot <" .. aSlot.Term_Index .. "> has dist " .. math.floor(currDist) .. " and _0 of <" .. aSlot.Term_Index_0 .. ">", 30)
if currDist < closestDist then
closestSlot = aSlot
closestDist = currDist
end
end
--trigger.action.outText("slot <" .. closestSlot.Term_Index .. "> has closest dist <" .. math.floor(closestDist) .. ">", 30)
return closestSlot
end
-- --
-- U N I T S M A N A G E M E N T -- U N I T S M A N A G E M E N T
-- --
@ -1823,6 +1887,7 @@ end
end end
function dcsCommon.stringStartsWith(theString, thePrefix) function dcsCommon.stringStartsWith(theString, thePrefix)
if not theString then return false end
return theString:find(thePrefix) == 1 return theString:find(thePrefix) == 1
end end
@ -2473,6 +2538,7 @@ function dcsCommon.flagArrayFromString(inString, verbose)
local rawElements = dcsCommon.splitString(inString, ",") local rawElements = dcsCommon.splitString(inString, ",")
-- go over all elements -- go over all elements
for idx, anElement in pairs(rawElements) do for idx, anElement in pairs(rawElements) do
anElement = dcsCommon.trim(anElement)
if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then
-- interpret this as a range -- interpret this as a range
local theRange = dcsCommon.splitString(anElement, "-") local theRange = dcsCommon.splitString(anElement, "-")
@ -2498,7 +2564,7 @@ function dcsCommon.flagArrayFromString(inString, verbose)
end end
else else
-- single number -- single number
f = dcsCommon.trim(anElement) -- DML flag upgrade: accept strings tonumber(anElement) local f = dcsCommon.trim(anElement) -- DML flag upgrade: accept strings tonumber(anElement)
if f then if f then
table.insert(flags, f) table.insert(flags, f)
@ -2513,6 +2579,81 @@ function dcsCommon.flagArrayFromString(inString, verbose)
return flags return flags
end end
function dcsCommon.rangeArrayFromString(inString, verbose)
if not verbose then verbose = false end
if verbose then
trigger.action.outText("+++rangeArray: processing <" .. inString .. ">", 30)
end
if string.len(inString) < 1 then
trigger.action.outText("+++rangeArray: empty ranges", 30)
return {}
end
local ranges = {}
local rawElements = dcsCommon.splitString(inString, ",")
-- go over all elements
for idx, anElement in pairs(rawElements) do
anElement = dcsCommon.trim(anElement)
local outRange = {}
if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then
-- interpret this as a range
local theRange = dcsCommon.splitString(anElement, "-")
local lowerBound = theRange[1]
lowerBound = tonumber(lowerBound)
local upperBound = theRange[2]
upperBound = tonumber(upperBound)
if lowerBound and upperBound then
-- swap if wrong order
if lowerBound > upperBound then
local temp = upperBound
upperBound = lowerBound
lowerBound = temp
end
-- now add to ranges
outRange[1] = lowerBound
outRange[2] = upperBound
table.insert(ranges, outRange)
if verbose then
trigger.action.outText("+++rangeArray: new range <" .. lowerBound .. "> to <" .. upperBound .. ">", 30)
end
else
-- bounds illegal
trigger.action.outText("+++rangeArray: ignored range <" .. anElement .. "> (range)", 30)
end
else
-- single number
local f = dcsCommon.trim(anElement)
f = tonumber(f)
if f then
outRange[1] = f
outRange[2] = f
table.insert(ranges, outRange)
if verbose then
trigger.action.outText("+++rangeArray: new (single-val) range <" .. f .. "> to <" .. f .. ">", 30)
end
else
trigger.action.outText("+++rangeArray: ignored element <" .. anElement .. "> (single)", 30)
end
end
end
if verbose then
trigger.action.outText("+++rangeArray: <" .. #ranges .. "> ranges total", 30)
end
return ranges
end
function dcsCommon.incFlag(flagName)
local v = trigger.misc.getUserFlag(flagName)
trigger.action.setUserFlag(flagName, v + 1)
end
function dcsCommon.decFlag(flagName)
local v = trigger.misc.getUserFlag(flagName)
trigger.action.setUserFlag(flagName, v - 1)
end
function dcsCommon.objectHandler(theObject, theCollector) function dcsCommon.objectHandler(theObject, theCollector)
table.insert(theCollector, theObject) table.insert(theCollector, theObject)
return true return true

View File

@ -1,5 +1,5 @@
groupTracker = {} groupTracker = {}
groupTracker.version = "1.2.0" groupTracker.version = "1.2.1"
groupTracker.verbose = false groupTracker.verbose = false
groupTracker.ups = 1 groupTracker.ups = 1
groupTracker.requiredLibs = { groupTracker.requiredLibs = {
@ -24,10 +24,11 @@ groupTracker.trackers = {}
- allGone! output - allGone! output
- triggerMethod - triggerMethod
- method - method
- isDead optimization - isDead optimiz ation
1.2.0 - double detection 1.2.0 - double detection
- numUnits output - numUnits output
- persistence - persistence
1.2.1 - allGone! bug removed
--]]-- --]]--
@ -362,7 +363,7 @@ function groupTracker.update()
-- see if we need to bang on empty! -- see if we need to bang on empty!
local currCount = #theZone.trackedGroups local currCount = #theZone.trackedGroups
if theZone.allGoneFlag and currCount == 0 and currCount ~= theZone.lastGroupCount then if theZone.allGoneFlag and currCount == 0 and currCount ~= theZone.lastGroupCount then
cfxZones.pollFlag(aZone.allGoneFlag, aZone.trackerMethod, aZone) cfxZones.pollFlag(theZone.allGoneFlag, theZone.trackerMethod, theZone)
end end
theZone.lastGroupCount = currCount theZone.lastGroupCount = currCount
end end

View File

@ -1,5 +1,5 @@
messenger = {} messenger = {}
messenger.version = "1.3.3" messenger.version = "2.0.0"
messenger.verbose = false messenger.verbose = false
messenger.requiredLibs = { messenger.requiredLibs = {
"dcsCommon", -- always "dcsCommon", -- always
@ -26,6 +26,22 @@ messenger.messengers = {}
- can interpret <lat>, <lon>, <mgrs> - can interpret <lat>, <lon>, <mgrs>
- zone-local verbosity - zone-local verbosity
1.3.3 - mute/messageMute option to start messenger in mute 1.3.3 - mute/messageMute option to start messenger in mute
2.0.0 - re-engineered message wildcards
- corrected dynamic content for time and latlon (classic)
- new timeFormat attribute
- <v: flagname>
- <t: flagname>
- added <ele>
- added imperial
- <lat: unit/zone>
- <lon: unit/zone>
- <ele: unit/zone>
- <mgrs: unit/zone>
- <latlon: unit/zone>
- <lle: unit/zone>
- messageError
- unit
- group
--]]-- --]]--
@ -48,6 +64,7 @@ end
-- read attributes -- read attributes
-- --
function messenger.preProcMessage(inMsg, theZone) function messenger.preProcMessage(inMsg, theZone)
-- Replace STATIC bits of message like CR and zone name
if not inMsg then return "<nil inMsg>" end if not inMsg then return "<nil inMsg>" end
local formerType = type(inMsg) local formerType = type(inMsg)
if formerType ~= "string" then inMsg = tostring(inMsg) end if formerType ~= "string" then inMsg = tostring(inMsg) end
@ -58,27 +75,172 @@ function messenger.preProcMessage(inMsg, theZone)
if theZone then if theZone then
outMsg = outMsg:gsub("<z>", theZone.name) outMsg = outMsg:gsub("<z>", theZone.name)
end end
return outMsg
end
-- Old-school processing to replace wildcards
-- repalce <t> with current time
-- replace <lat> with zone's current lonlat
-- replace <mgrs> with zone's current mgrs
function messenger.dynamicProcessClassic(inMsg, theZone)
if not inMsg then return "<nil inMsg>" end
-- replace <t> with current mission time HMS -- replace <t> with current mission time HMS
local absSecs = timer.getAbsTime()-- + env.mission.start_time local absSecs = timer.getAbsTime()-- + env.mission.start_time
while absSecs > 86400 do while absSecs > 86400 do
absSecs = absSecs - 86400 -- subtract out all days absSecs = absSecs - 86400 -- subtract out all days
end end
local timeString = dcsCommon.processHMS("<:h>:<:m>:<:s>", absSecs) local timeString = dcsCommon.processHMS(theZone.msgTimeFormat, absSecs)
outMsg = outMsg:gsub("<t>", timeString) local outMsg = inMsg:gsub("<t>", timeString)
-- replace <lat> with lat of zone point and <lon> with lon of zone point -- replace <lat> with lat of zone point and <lon> with lon of zone point
-- and <mgrs> with mgrs coords of zone point -- and <mgrs> with mgrs coords of zone point
local currPoint = cfxZones.getPoint(theZone) local currPoint = cfxZones.getPoint(theZone)
local lat, lon, alt = coord.LOtoLL(currPoint) local lat, lon = coord.LOtoLL(currPoint)
lat, lon = dcsCommon.latLon2Text(lat, lon) lat, lon = dcsCommon.latLon2Text(lat, lon)
local alt = land.getHeight({x = currPoint.x, y = currPoint.z})
if theZone.imperialUnits then
alt = math.floor(alt * 3.28084) -- feet
else
alt = math.floor(alt) -- meters
end
outMsg = outMsg:gsub("<lat>", lat) outMsg = outMsg:gsub("<lat>", lat)
outMsg = outMsg:gsub("<lon>", lon) outMsg = outMsg:gsub("<lon>", lon)
outMsg = outMsg:gsub("<ele>", alt)
local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint))
local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
outMsg = outMsg:gsub("<mgrs>", mgrs) outMsg = outMsg:gsub("<mgrs>", mgrs)
return outMsg return outMsg
end end
--
-- new dynamic flag processing
--
function messenger.processDynamicValues(inMsg, theZone)
-- replace all occurences of <v: flagName> with their values
local pattern = "<v:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
local outMsg = inMsg
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<v:%s*", "")
param = string.gsub(param, ">","")
-- param = dcsCommon.trim(param) -- trim is called anyway
-- access flag
local val = cfxZones.getFlagValue(param, theZone)
val = tostring(val)
if not val then val = "NULL" end
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, val, 1) -- only one sub!
end
until not startLoc
return outMsg
end
function messenger.processDynamicTime(inMsg, theZone)
-- replace all occurences of <v: flagName> with their values
local pattern = "<t:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
local outMsg = inMsg
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<t:%s*", "")
param = string.gsub(param, ">","")
-- access flag
local val = cfxZones.getFlagValue(param, theZone)
-- use this to process as time value
--trigger.action.outText("time: accessing <" .. param .. "> and received <" .. val .. ">", 30)
local timeString = dcsCommon.processHMS(theZone.msgTimeFormat, val)
if not timeString then timeString = "NULL" end
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, timeString, 1) -- only one sub!
end
until not startLoc
return outMsg
end
function messenger.processDynamicLoc(inMsg, theZone)
-- replace all occurences of <lat/lon/ele/mgrs: flagName> with their values
local locales = {"lat", "lon", "ele", "mgrs", "lle", "latlon"}
local outMsg = inMsg
for idx, aLocale in pairs(locales) do
local pattern = "<" .. aLocale .. ":%s*[%s%w%*%d%.%-_]+>"
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<" .. aLocale .. ":%s*", "")
param = string.gsub(param, ">","")
-- find zone or unit
param = dcsCommon.trim(param)
local thePoint = nil
local tZone = cfxZones.getZoneByName(param)
local tUnit = Unit.getByName(param)
if tZone then
thePoint = cfxZones.getPoint(tZone)
-- since zones always have elevation of 0,
-- now get the elevation from the map
thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z})
elseif tUnit then
if Unit.isExist(tUnit) then
thePoint = tUnit:getPoint()
end
else
-- nothing to do, remove me.
end
local locString = theZone.errString
if thePoint then
-- now that we have a point, we can do locale-specific
-- processing. return result in locString
local lat, lon, alt = coord.LOtoLL(thePoint)
lat, lon = dcsCommon.latLon2Text(lat, lon)
if theZone.imperialUnits then
alt = math.floor(alt * 3.28084) -- feet
else
alt = math.floor(alt) -- meters
end
if aLocale == "lat" then locString = lat
elseif aLocale == "lon" then locString = lon
elseif aLocale == "ele" then locString = tostring(alt)
elseif aLocale == "lle" then locString = lat .. " " .. lon .. " ele " .. tostring(alt)
elseif aLocale == "latlon" then locString = lat .. " " .. lon
else
-- we have mgrs
local grid = coord.LLtoMGRS(coord.LOtoLL(thePoint))
locString = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
end
end
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, locString, 1) -- only one sub!
end -- if startloc
until not startLoc
end -- for all locales
return outMsg
end
function messenger.dynamicFlagProcessing(inMsg, theZone)
if not inMsg then return "No in message" end
if not theZone then return "Nil zone" end
-- process <v: xxx>
local msg = messenger.processDynamicValues(inMsg, theZone)
-- process <t: xxx>
msg = messenger.processDynamicTime(msg, theZone)
-- process lat / lon / ele / mgrs
msg = messenger.processDynamicLoc(msg, theZone)
return msg
end
function messenger.createMessengerWithZone(theZone) function messenger.createMessengerWithZone(theZone)
-- start val - a range -- start val - a range
@ -147,6 +309,7 @@ function messenger.createMessengerWithZone(theZone)
theZone.lastMessageOn = cfxZones.getFlagValue(theZone.messageOnFlag, theZone) theZone.lastMessageOn = cfxZones.getFlagValue(theZone.messageOnFlag, theZone)
end end
-- reveiver: coalition, group, unit
if cfxZones.hasProperty(theZone, "coalition") then if cfxZones.hasProperty(theZone, "coalition") then
theZone.msgCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0) theZone.msgCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0)
end end
@ -155,11 +318,45 @@ function messenger.createMessengerWithZone(theZone)
theZone.msgCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "msgCoalition", 0) theZone.msgCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "msgCoalition", 0)
end end
-- flag whose value can be read if cfxZones.hasProperty(theZone, "group") then
theZone.msgGroup = cfxZones.getStringFromZoneProperty(theZone, "group", "<none>")
end
if cfxZones.hasProperty(theZone, "msgGroup") then
theZone.msgGroup = cfxZones.getStringFromZoneProperty(theZone, "msgGroup", "<none>")
end
if cfxZones.hasProperty(theZone, "unit") then
theZone.msgUnit = cfxZones.getStringFromZoneProperty(theZone, "unit", "<none>")
end
if cfxZones.hasProperty(theZone, "msgUnit") then
theZone.msgUnit = cfxZones.getStringFromZoneProperty(theZone, "msgUnit", "<none>")
end
if (theZone.msgGroup and theZone.msgUnit) or
(theZone.msgGroup and theZone.msgCoalition) or
(theZone.msgUnit and theZone.msgCoalition)
then
trigger.action.outText("+++msg: WARNING - messenger in <" .. theZone.name .. "> has conflicting coalition, group and unit, use only one.", 30)
end
-- flag whose value can be read: to be deprecated
if cfxZones.hasProperty(theZone, "messageValue?") then if cfxZones.hasProperty(theZone, "messageValue?") then
theZone.messageValue = cfxZones.getStringFromZoneProperty(theZone, "messageValue?", "<none>") theZone.messageValue = cfxZones.getStringFromZoneProperty(theZone, "messageValue?", "<none>")
end end
-- time format for new <t: flagname>
theZone.msgTimeFormat = cfxZones.getStringFromZoneProperty(theZone, "timeFormat", "<:h>:<:m>:<:s>")
theZone.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperial", false)
if cfxZones.hasProperty(theZone, "imperialUnits") then
theZone.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperialUnits", false)
end
theZone.errString = cfxZones.getStringFromZoneProperty(theZone, "error", "")
if cfxZones.hasProperty(theZone, "messageError") then
theZone.errString = cfxZones.getStringFromZoneProperty(theZone, "messageError", "")
end
if messenger.verbose or theZone.verbose then if messenger.verbose or theZone.verbose then
trigger.action.outText("+++Msg: new zone <".. theZone.name .."> will say <".. theZone.message .. ">", 30) trigger.action.outText("+++Msg: new zone <".. theZone.name .."> will say <".. theZone.message .. ">", 30)
end end
@ -182,12 +379,22 @@ function messenger.getMessage(theZone)
-- replace *zone and *value wildcards -- replace *zone and *value wildcards
msg = string.gsub(msg, "*name", zName) --msg = string.gsub(msg, "*name", zName)-- deprecated
msg = string.gsub(msg, "*value", zVal) --msg = string.gsub(msg, "*value", zVal) -- deprecated
-- old-school <v> to provide value from messageValue
msg = string.gsub(msg, "<v>", zVal) msg = string.gsub(msg, "<v>", zVal)
local z = tonumber(zVal) local z = tonumber(zVal)
if not z then z = 0 end if not z then z = 0 end
msg = dcsCommon.processHMS(msg, z) msg = dcsCommon.processHMS(msg, z)
-- process <t> [classic format], <latlon> and <mrgs>
msg = messenger.dynamicProcessClassic(msg, theZone)
-- now add new processing of <x: flagname> access
msg = messenger.dynamicFlagProcessing(msg, theZone)
-- now add new processinf of <lat: flagname>
-- also handles <lon:x>, <ele:x>, <mgrs:x>
return msg return msg
end end
@ -212,6 +419,20 @@ function messenger.isTriggered(theZone)
if theZone.msgCoalition then if theZone.msgCoalition then
trigger.action.outTextForCoalition(theZone.msgCoalition, msg, theZone.duration, theZone.clearScreen) trigger.action.outTextForCoalition(theZone.msgCoalition, msg, theZone.duration, theZone.clearScreen)
trigger.action.outSoundForCoalition(theZone.msgCoalition, fileName) trigger.action.outSoundForCoalition(theZone.msgCoalition, fileName)
elseif theZone.msgGroup then
local theGroup = Group.getByName(theZone.msgGroup)
if theGroup and Group.isExist(theGroup) then
local ID = theGroup:getID()
trigger.action.outTextForGroup(ID, msg, theZone.duration, theZone.clearScreen)
trigger.action.outSoundForGroup(ID, fileName)
end
elseif theZone.msgUnit then
local theUnit = Unit.getByName(theZone.msgUnit)
if theUnit and Unit.isExist(theUnit) then
local ID = theUnit:getID()
trigger.action.outTextForUnit(ID, msg, theZone.duration, theZone.clearScreen)
trigger.action.outSoundForUnit(ID, fileName)
end
else else
-- out to all -- out to all
trigger.action.outText(msg, theZone.duration, theZone.clearScreen) trigger.action.outText(msg, theZone.duration, theZone.clearScreen)
@ -312,5 +533,10 @@ end
--[[-- --[[--
Wildcard extension: Wildcard extension:
messageValue supports multiple flags like 1-3, *hi ther, *bingo and then *value[name] returns that value - general flag access <v flag name>
- <t: flag name>
- <lat: unit/zone name>
- <mrgs: unit/zone name>
--]]-- --]]--

View File

@ -1,5 +1,5 @@
radioMenu = {} radioMenu = {}
radioMenu.version = "1.1.0" radioMenu.version = "2.0.0"
radioMenu.verbose = false radioMenu.verbose = false
radioMenu.ups = 1 radioMenu.ups = 1
radioMenu.requiredLibs = { radioMenu.requiredLibs = {
@ -15,8 +15,18 @@ radioMenu.menus = {}
1.1.0 removeMenu 1.1.0 removeMenu
addMenu addMenu
menuVisible menuVisible
2.0.0 redesign: handles multiple receivers
optional MX module
group option
type option
multiple group names
multiple types
gereric helo type
generic plane type
type works with coalition
--]]-- --]]--
function radioMenu.addRadioMenu(theZone) function radioMenu.addRadioMenu(theZone)
table.insert(radioMenu.menus, theZone) table.insert(radioMenu.menus, theZone)
end end
@ -35,44 +45,192 @@ end
-- --
-- read zone -- read zone
-- --
function radioMenu.installMenu(theZone) function radioMenu.filterPlayerIDForType(theZone)
if theZone.coalition == 0 then -- note: we currently ignore coalition
theZone.rootMenu = missionCommands.addSubMenu(theZone.rootName, nil) local theIDs = {}
local allTypes = {}
if dcsCommon.containsString(theZone.menuTypes, ",") then
allTypes = dcsCommon.splitString(theZone.menuTypes, ",")
else else
theZone.rootMenu = missionCommands.addSubMenuForCoalition(theZone.coalition, theZone.rootName, nil) table.insert(allTypes, theZone.menuTypes)
end end
local menuA = cfxZones.getStringFromZoneProperty(theZone, "itemA", "<no A submenu>") -- now iterate all types, and include any player that matches
if theZone.coalition == 0 then -- note that a player may match twice, so we use a dict instead of an
theZone.menuA = missionCommands.addCommand(menuA, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "A"}) -- array. Since we later iterate ID by idx, that's not an issue
for idx, aType in pairs(allTypes) do
local theType = dcsCommon.trim(aType)
local lowerType = string.lower(theType)
for gName, gData in pairs(cfxMX.playerGroupByName) do
-- get coalition of group
local coa = cfxMX.groupCoalitionByName[gName]
if (theZone.coalition == 0 or theZone.coalition == coa) then
-- do special types first
if dcsCommon.stringStartsWith(lowerType, "helo") or dcsCommon.stringStartsWith(lowerType, "heli") then
-- we look for all helicoperts
if cfxMX.groupTypeByName[gName] == "helicopter" then
theIDs[gName] = gData.groupId
if theZone.verbose or radioMenu.verbose then
trigger.action.outText("+++menu: Player Group <" .. gName .. "> matches gen-type helicopter", 30)
end
end
elseif lowerType == "plane" or lowerType == "planes" then
-- we look for all planes
if cfxMX.groupTypeByName[gName] == "plane" then
theIDs[gName] = gData.groupId
if theZone.verbose or radioMenu.verbose then
trigger.action.outText("+++menu: Player Group <" .. gName .. "> matches gen-type plane", 30)
end
end
else
-- we are looking for a particular type, e.g. A-10A
-- since groups do not carry the type, but all player
-- groups are of the same type, we access the first
-- unit. Note that this may later break if ED implement
-- player groups of mixed type
if gData.units and gData.units[1] and gData.units[1].type == theType then
theIDs[gName] = gData.groupId
if theZone.verbose or radioMenu.verbose then
trigger.action.outText("+++menu: Player Group <" .. gName .. "> matches type <" .. theType .. ">", 30)
end
else
end
end
else
if theZone.verbose or radioMenu.verbose then
trigger.action.outText("+++menu: type check failed coalition for <" .. gName .. ">", 30)
end
end
end
end
return theIDs
end
function radioMenu.filterPlayerIDForGroup(theZone)
-- create an iterable list of groups, separated by commas
-- note that we could introduce wildcards for groups later
local theIDs = {}
local allGroups = {}
if dcsCommon.containsString(theZone.menuGroup, ",") then
allGroups = dcsCommon.splitString(theZone.menuGroup, ",")
else else
theZone.menuA = missionCommands.addCommandForCoalition(theZone.coalition, menuA, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "A"}) table.insert(allGroups, theZone.menuGroup)
end
for idx, gName in pairs(allGroups) do
gName = dcsCommon.trim(gName)
local theGroup = cfxMX.playerGroupByName[gName]
if theGroup then
local gID = theGroup.groupId
table.insert(theIDs, gID)
if theZone.verbose or radioMenu.verbose then
trigger.action.outText("+++menu: Player Group <" .. gName .. "> found: <" .. gID .. ">", 30)
end
else
trigger.action.outText("+++menu: Player Group <" .. gName .. "> does not exist", 30)
end
end
return theIDs
end
function radioMenu.installMenu(theZone)
-- local theGroup = 0 -- was: nil
local gID = nil
if theZone.menuGroup then
if not cfxMX then
trigger.action.outText("WARNING: radioMenu's group attribute requires the 'cfxMX' module", 30)
return
end
-- access cfxMX player info for group ID
gID = radioMenu.filterPlayerIDForGroup(theZone)
elseif theZone.menuTypes then
if not cfxMX then
trigger.action.outText("WARNING: radioMenu's type attribute requires the 'cfxMX' module", 30)
return
end
-- access cxfMX player infor with type match for ID
gID = radioMenu.filterPlayerIDForType(theZone)
end
theZone.rootMenu = {}
theZone.mcdA = {}
theZone.mcdB = {}
theZone.mcdC = {}
theZone.mcdD = {}
theZone.mcdA[0] = 0
theZone.mcdB[0] = 0
theZone.mcdC[0] = 0
theZone.mcdD[0] = 0
if theZone.menuGroup or theZone.menuTypes then
for idx, grp in pairs(gID) do
local aRoot = missionCommands.addSubMenuForGroup(grp, theZone.rootName, nil)
theZone.rootMenu[grp] = aRoot
theZone.mcdA[grp] = 0
theZone.mcdB[grp] = 0
theZone.mcdC[grp] = 0
theZone.mcdD[grp] = 0
end
elseif theZone.coalition == 0 then
theZone.rootMenu[0] = missionCommands.addSubMenu(theZone.rootName, nil)
else
theZone.rootMenu[0] = missionCommands.addSubMenuForCoalition(theZone.coalition, theZone.rootName, nil)
end
if cfxZones.hasProperty(theZone, "itemA") then
local menuA = cfxZones.getStringFromZoneProperty(theZone, "itemA", "<no A submenu>")
if theZone.menuGroup or theZone.menuTypes then
theZone.menuA = {}
for idx, grp in pairs(gID) do
theZone.menuA[grp] = missionCommands.addCommandForGroup(grp, menuA, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "A", grp})
end
elseif theZone.coalition == 0 then
theZone.menuA = missionCommands.addCommand(menuA, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "A"})
else
theZone.menuA = missionCommands.addCommandForCoalition(theZone.coalition, menuA, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "A"})
end
end end
if cfxZones.hasProperty(theZone, "itemB") then if cfxZones.hasProperty(theZone, "itemB") then
local menuB = cfxZones.getStringFromZoneProperty(theZone, "itemB", "<no B submenu>") local menuB = cfxZones.getStringFromZoneProperty(theZone, "itemB", "<no B submenu>")
if theZone.coalition == 0 then if theZone.menuGroup or theZone.menuTypes then
theZone.menuB = missionCommands.addCommand(menuB, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "B"}) for idx, grp in pairs(gID) do
theZone.menuB[grp] = missionCommands.addCommandForGroup(grp, menuB, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "B"})
end
elseif theZone.coalition == 0 then
theZone.menuB = missionCommands.addCommand(menuB, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "B"})
else else
theZone.menuB = missionCommands.addCommandForCoalition(theZone.coalition, menuB, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "B"}) theZone.menuB = missionCommands.addCommandForCoalition(theZone.coalition, menuB, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "B"})
end end
end end
if cfxZones.hasProperty(theZone, "itemC") then if cfxZones.hasProperty(theZone, "itemC") then
local menuC = cfxZones.getStringFromZoneProperty(theZone, "itemC", "<no C submenu>") local menuC = cfxZones.getStringFromZoneProperty(theZone, "itemC", "<no C submenu>")
if theZone.coalition == 0 then if theZone.menuGroup or theZone.menuTypes then
theZone.menuC = missionCommands.addCommand(menuC, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "C"}) for idx, grp in pairs(gID) do
theZone.menuC[grp] = missionCommands.addCommandForGroup(grp, menuC, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "C"})
end
elseif theZone.coalition == 0 then
theZone.menuC = missionCommands.addCommand(menuC, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "C"})
else else
theZone.menuC = missionCommands.addCommandForCoalition(theZone.coalition, menuC, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "C"}) theZone.menuC = missionCommands.addCommandForCoalition(theZone.coalition, menuC, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "C"})
end end
end end
if cfxZones.hasProperty(theZone, "itemD") then if cfxZones.hasProperty(theZone, "itemD") then
local menuD = cfxZones.getStringFromZoneProperty(theZone, "itemD", "<no D submenu>") local menuD = cfxZones.getStringFromZoneProperty(theZone, "itemD", "<no D submenu>")
if theZone.coalition == 0 then if theZone.menuGroup or theZone.menuTypes then
theZone.menuD = missionCommands.addCommand(menuD, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "D"}) for idx, grp in pairs(gID) do
theZone.menuD[grp] = missionCommands.addCommandForGroup(grp, menuD, theZone.rootMenu[grp], radioMenu.redirectMenuX, {theZone, "D"})
end
elseif theZone.coalition == 0 then
theZone.menuD = missionCommands.addCommand(menuD, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "D"})
else else
theZone.menuD = missionCommands.addCommandForCoalition(theZone.coalition, menuD, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "D"}) theZone.menuD = missionCommands.addCommandForCoalition(theZone.coalition, menuD, theZone.rootMenu[0], radioMenu.redirectMenuX, {theZone, "D"})
end end
end end
end end
@ -81,6 +239,18 @@ function radioMenu.createRadioMenuWithZone(theZone)
theZone.rootName = cfxZones.getStringFromZoneProperty(theZone, "radioMenu", "<No Name>") theZone.rootName = cfxZones.getStringFromZoneProperty(theZone, "radioMenu", "<No Name>")
theZone.coalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0) theZone.coalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0)
-- groups / types
if cfxZones.hasProperty(theZone, "group") then
theZone.menuGroup = cfxZones.getStringFromZoneProperty(theZone, "group", "<none>")
theZone.menuGroup = dcsCommon.trim(theZone.menuGroup)
elseif cfxZones.hasProperty(theZone, "groups") then
theZone.menuGroup = cfxZones.getStringFromZoneProperty(theZone, "groups", "<none>")
theZone.menuGroup = dcsCommon.trim(theZone.menuGroup)
elseif cfxZones.hasProperty(theZone, "type") then
theZone.menuTypes = cfxZones.getStringFromZoneProperty(theZone, "type", "none")
elseif cfxZones.hasProperty(theZone, "types") then
theZone.menuTypes = cfxZones.getStringFromZoneProperty(theZone, "types", "none")
end
theZone.menuVisible = cfxZones.getBoolFromZoneProperty(theZone, "menuVisible", true) theZone.menuVisible = cfxZones.getBoolFromZoneProperty(theZone, "menuVisible", true)
@ -99,22 +269,22 @@ function radioMenu.createRadioMenuWithZone(theZone)
theZone.itemAChosen = cfxZones.getStringFromZoneProperty(theZone, "A!", "*<none>") theZone.itemAChosen = cfxZones.getStringFromZoneProperty(theZone, "A!", "*<none>")
theZone.cooldownA = cfxZones.getNumberFromZoneProperty(theZone, "cooldownA", 0) theZone.cooldownA = cfxZones.getNumberFromZoneProperty(theZone, "cooldownA", 0)
theZone.mcdA = 0 --theZone.mcdA = 0
theZone.busyA = cfxZones.getStringFromZoneProperty(theZone, "busyA", "Please stand by (<s> seconds)") theZone.busyA = cfxZones.getStringFromZoneProperty(theZone, "busyA", "Please stand by (<s> seconds)")
theZone.itemBChosen = cfxZones.getStringFromZoneProperty(theZone, "B!", "*<none>") theZone.itemBChosen = cfxZones.getStringFromZoneProperty(theZone, "B!", "*<none>")
theZone.cooldownB = cfxZones.getNumberFromZoneProperty(theZone, "cooldownB", 0) theZone.cooldownB = cfxZones.getNumberFromZoneProperty(theZone, "cooldownB", 0)
theZone.mcdB = 0 --theZone.mcdB = 0
theZone.busyB = cfxZones.getStringFromZoneProperty(theZone, "busyB", "Please stand by (<s> seconds)") theZone.busyB = cfxZones.getStringFromZoneProperty(theZone, "busyB", "Please stand by (<s> seconds)")
theZone.itemCChosen = cfxZones.getStringFromZoneProperty(theZone, "C!", "*<none>") theZone.itemCChosen = cfxZones.getStringFromZoneProperty(theZone, "C!", "*<none>")
theZone.cooldownC = cfxZones.getNumberFromZoneProperty(theZone, "cooldownC", 0) theZone.cooldownC = cfxZones.getNumberFromZoneProperty(theZone, "cooldownC", 0)
theZone.mcdC = 0 --theZone.mcdC = 0
theZone.busyC = cfxZones.getStringFromZoneProperty(theZone, "busyC", "Please stand by (<s> seconds)") theZone.busyC = cfxZones.getStringFromZoneProperty(theZone, "busyC", "Please stand by (<s> seconds)")
theZone.itemDChosen = cfxZones.getStringFromZoneProperty(theZone, "D!", "*<none>") theZone.itemDChosen = cfxZones.getStringFromZoneProperty(theZone, "D!", "*<none>")
theZone.cooldownD = cfxZones.getNumberFromZoneProperty(theZone, "cooldownD", 0) theZone.cooldownD = cfxZones.getNumberFromZoneProperty(theZone, "cooldownD", 0)
theZone.mcdD = 0 --theZone.mcdD = 0
theZone.busyD = cfxZones.getStringFromZoneProperty(theZone, "busyD", "Please stand by (<s> seconds)") theZone.busyD = cfxZones.getStringFromZoneProperty(theZone, "busyD", "Please stand by (<s> seconds)")
if cfxZones.hasProperty(theZone, "removeMenu?") then if cfxZones.hasProperty(theZone, "removeMenu?") then
@ -160,24 +330,43 @@ function radioMenu.redirectMenuX(args)
timer.scheduleFunction(radioMenu.doMenuX, args, timer.getTime() + 0.1) timer.scheduleFunction(radioMenu.doMenuX, args, timer.getTime() + 0.1)
end end
function radioMenu.cdByGID(cd, theZone, gID)
if not gID then gID = 0 end
--if not gID then return cd[0] end
return cd[gID]
end
function radioMenu.setCDByGID(cd, theZone, gID, newVal)
if not gID then gID = 0 end
--theZone[cd] = newVal
--
--end
local allCD = theZone[cd]
allCD[gID] = newVal
theZone[cd] = allCD
end
function radioMenu.doMenuX(args) function radioMenu.doMenuX(args)
theZone = args[1] theZone = args[1]
theItemIndex = args[2] -- A, B , C .. ? theItemIndex = args[2] -- A, B , C .. ?
local cd = theZone.mcdA theGroup = args[3] -- can be nil or groupID
if not theGroup then theGroup = 0 end
local cd = radioMenu.cdByGID(theZone.mcdA, theZone, theGroup) --theZone.mcdA
local busy = theZone.busyA local busy = theZone.busyA
local theFlag = theZone.itemAChosen local theFlag = theZone.itemAChosen
-- decode A..X -- decode A..X
if theItemIndex == "B"then if theItemIndex == "B"then
cd = theZone.mcdB cd = radioMenu.cdByGID(theZone.mcdB, theZone, theGroup) -- theZone.mcdB
busy = theZone.busyB busy = theZone.busyB
theFlag = theZone.itemBChosen theFlag = theZone.itemBChosen
elseif theItemIndex == "C" then elseif theItemIndex == "C" then
cd = theZone.mcdC cd = radioMenu.cdByGID(theZone.mcdC, theZone, theGroup) -- theZone.mcdC
busy = theZone.busyC busy = theZone.busyC
theFlag = theZone.itemCChosen theFlag = theZone.itemCChosen
elseif theItemIndex == "D" then elseif theItemIndex == "D" then
cd = theZone.mcdD cd = radioMenu.cdByGID(theZone.mcdD, theZone, theGroup) -- theZone.mcdD
busy = theZone.busyD busy = theZone.busyD
theFlag = theZone.itemDChosen theFlag = theZone.itemDChosen
end end
@ -193,13 +382,13 @@ function radioMenu.doMenuX(args)
-- set new cooldown -- needs own decoder A..X -- set new cooldown -- needs own decoder A..X
if theItemIndex == "A" then if theItemIndex == "A" then
theZone.mcdA = now + theZone.cooldownA radioMenu.setCDByGID("mcdA", theZone, theGroup, now + theZone.cooldownA)
elseif theItemIndex == "B" then elseif theItemIndex == "B" then
theZone.mcdB = now + theZone.cooldownB radioMenu.setCDByGID("mcdB", theZone, theGroup, now + theZone.cooldownB)
elseif theItemIndex == "C" then elseif theItemIndex == "C" then
theZone.mcdC = now + theZone.cooldownC radioMenu.setCDByGID("mcdC", theZone, theGroup, now + theZone.cooldownC)
else else
theZone.mcdD = now + theZone.cooldownD radioMenu.setCDByGID("mcdC", theZone, theGroup, now + theZone.cooldownC)
end end
cfxZones.pollFlag(theFlag, theZone.radioMethod, theZone) cfxZones.pollFlag(theFlag, theZone.radioMethod, theZone)
@ -208,10 +397,10 @@ function radioMenu.doMenuX(args)
end end
end end
-- --
-- Update -- required when we can enable/disable a zone's menu -- Update -- required when we can enable/disable a zone's menu
-- --
function radioMenu.update() function radioMenu.update()
-- call me in a second to poll triggers -- call me in a second to poll triggers
timer.scheduleFunction(radioMenu.update, {}, timer.getTime() + 1/radioMenu.ups) timer.scheduleFunction(radioMenu.update, {}, timer.getTime() + 1/radioMenu.ups)
@ -221,15 +410,15 @@ function radioMenu.update()
if theZone.removeMenu if theZone.removeMenu
and cfxZones.testZoneFlag(theZone, theZone.removeMenu, theZone.radioTriggerMethod, "lastRemoveMenu") and cfxZones.testZoneFlag(theZone, theZone.removeMenu, theZone.radioTriggerMethod, "lastRemoveMenu")
and theZone.menuVisible and theZone.menuVisible
then then
if theZone.verbose or radioMenu.verbose then if theZone.menuGroup or theZone.menuTypes then
trigger.action.outText("+++menu: removing <" .. dcsCommon.menu2text(theZone.rootMenu) .. "> for <" .. theZone.name .. ">", 30) for gID, aRoot in pairs(theZone.rootMenu) do
end missionCommands.removeItemForGroup(gID, aRoot)
end
if theZone.coalition == 0 then elseif theZone.coalition == 0 then
missionCommands.removeItem(theZone.rootMenu) missionCommands.removeItem(theZone.rootMenu[0])
else else
missionCommands.removeItemForCoalition(theZone.coalition, theZone.rootMenu) missionCommands.removeItemForCoalition(theZone.coalition, theZone.rootMenu[0])
end end
theZone.menuVisible = false theZone.menuVisible = false
@ -304,5 +493,6 @@ if not radioMenu.start() then
end end
--[[-- --[[--
callbacks for the menus callbacks for the menus
check CD/standby code for multiple groups
--]]-- --]]--

440
modules/sequencer.lua Normal file
View File

@ -0,0 +1,440 @@
sequencer = {}
sequencer.version = "1.0.0"
sequencer.verbose = false
sequencer.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
--[[--
Sequencer: pull flags in a sequence with oodles of features
Copyright (c) 2022 by Christian Franz
--]]--
sequencer.sequencers = {}
function sequencer.addSequencer(theZone)
if not theZone then return end
table.insert(sequencer.sequencers, theZone)
end
function sequencer.getSequenceByName(aName)
if not aName then return nil end
for idx, aZone in pairs(sequencer.sequencers) do
if aZone.name == aName then return aZone end
end
return nil
end
--
-- read from ME
--
function sequencer.createSequenceWithZone(theZone)
local seqRaw = cfxZones.getStringFromZoneProperty(theZone, "sequence!", "none")
local theFlags = dcsCommon.flagArrayFromString(seqRaw)
theZone.sequence = theFlags
local interRaw = cfxZones.getStringFromZoneProperty(theZone, "intervals", "86400")
if cfxZones.hasProperty(theZone, "interval") then
interRaw = cfxZones.getStringFromZoneProperty(theZone, "interval", "86400") -- = 24 * 3600 = 24 hours default interval
end
local theIntervals = dcsCommon.rangeArrayFromString(interRaw, false)
theZone.intervals = theIntervals
theZone.seqIndex = 1 -- we start at one
theZone.intervalIndex = 1 -- here too
theZone.onStart = cfxZones.getBoolFromZoneProperty(theZone, "onStart", false)
theZone.zeroSequence = cfxZones.getBoolFromZoneProperty(theZone, "zeroSequence", true)
theZone.seqLoop = cfxZones.getBoolFromZoneProperty(theZone, "loop", false)
theZone.seqRunning = false
theZone.seqComplete = false
theZone.seqStarted = false
theZone.timeLimit = 0 -- will be set to when we expire
if cfxZones.hasProperty(theZone, "done!") then
theZone.seqDone = cfxZones.getStringFromZoneProperty(theZone, "done!", "<none>")
elseif cfxZones.hasProperty(theZone, "seqDone!") then
theZone.seqDone = cfxZones.getStringFromZoneProperty(theZone, "seqDone!", "<none>")
end
if cfxZones.hasProperty(theZone, "next?") then
theZone.nextSeq = cfxZones.getStringFromZoneProperty(theZone, "next?", "<none>")
theZone.lastNextSeq = cfxZones.getFlagValue(theZone.nextSeq, theZone)
end
if cfxZones.hasProperty(theZone, "startSeq?") then
theZone.startSeq = cfxZones.getStringFromZoneProperty(theZone, "startSeq?", "<none>")
theZone.lastStartSeq = cfxZones.getFlagValue(theZone.startSeq, theZone)
--trigger.action.outText("read as " .. theZone.startSeq, 30)
end
if cfxZones.hasProperty(theZone, "stopSeq?") then
theZone.stopSeq = cfxZones.getStringFromZoneProperty(theZone, "stopSeq?", "<none>")
theZone.lastStopSeq = cfxZones.getFlagValue(theZone.stopSeq, theZone)
end
if cfxZones.hasProperty(theZone, "resetSeq?") then
theZone.resetSeq = cfxZones.getStringFromZoneProperty(theZone, "resetSeq?", "<none>")
theZone.lastResetSeq = cfxZones.getFlagValue(theZone.resetSeq, theZone)
end
-- methods
theZone.seqMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc")
if cfxZones.hasProperty(theZone, "seqMethod") then
theZone.seqMethod = cfxZones.getStringFromZoneProperty(theZone, "seqMethod", "inc")
end
theZone.seqTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change")
if cfxZones.hasProperty(theZone, "seqTriggerMethod") then
theZone.seqTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "seqTriggerMethod", "change")
end
if (not theZone.onStart) and not (theZone.startSeq) then
trigger.action.outText("+++seq: WARNING - sequence <" .. theZone.name .. "> cannot be started: no startSeq? and onStart is false", 30)
end
end
function sequencer.fire(theZone)
-- time's up. poll flag at index
local theFlag = theZone.sequence[theZone.seqIndex]
if theFlag then
cfxZones.pollFlag(theFlag, theZone.seqMethod, theZone)
if theZone.verbose or sequencer.verbose then
trigger.action.outText("+++seq: triggering flag <" .. theFlag .. "> for index <" .. theZone.seqIndex .. "> in sequence <" .. theZone.name .. ">", 30)
end
else
trigger.action.outText("+++seq: ran out of sequences for <" .. theZone.name .. "> on index <" .. theZone.seqIndex .. ">", 30)
end
end
function sequencer.advanceInterval(theZone)
theZone.intervalIndex = theZone.intervalIndex + 1
if theZone.intervalIndex > #theZone.intervals then
theZone.intervalIndex = 1 -- always loops
end
end
function sequencer.advanceSeq(theZone)
-- get the next index for the sequence
theZone.seqIndex = theZone.seqIndex + 1
-- loop if over and enabled
if theZone.seqIndex > #theZone.sequence then
if theZone.seqLoop then
theZone.seqIndex = 1
else
return false
end
end
-- returns true if success
return true
end
function sequencer.startWaitCycle(theZone)
if theZone.seqComplete then return end
local bounds = theZone.intervals[theZone.intervalIndex]
local newInterval = dcsCommon.randomBetween(bounds[1], bounds[2])
theZone.timeLimit = timer.getTime() + newInterval
if theZone.verbose or sequencer.verbose then
trigger.action.outText("+++seq: start wait for <" .. newInterval .. "> in sequence <" .. theZone.name .. ">", 30)
end
end
function sequencer.pause(theZone)
if theZone.seqComplete then return end
if not theZone.seqRunning then return end
local now = timer.getTime()
theZone.timeRemaining = theZone.timeLimit - now
theZone.seqRunning = false
end
function sequencer.continue(theZone)
if theZone.seqComplete then return end -- Frankie says: no more
if theZone.seqRunning then return end -- we are already running
-- reset any lingering 'next' flags so they don't
-- trigger a newly started sequence
if theZone.nextSeq then
theZone.lastNextSeq = cfxZones.getFlagValue(theZone.nextSeq, theZone)
end
if not theZone.seqStarted then
-- this is the very first time we are running.
if theZone.zeroSequence then
-- start with a bang
sequencer.fire(theZone)
sequencer.advanceSeq(theZone)
end
theZone.seqRunning = true
theZone.seqStarted = true
sequencer.startWaitCycle(theZone)
return
end
-- we are continuing a paused sequencer
local now = timer.getTime()
if not theZone.timeRemaining then theZone.timeRemaining = 1 end
theZone.timeLimit = now + theZone.timeRemaining
theZone.seqRunning = true
end
function sequencer.reset(theZone)
theZone.seqComplete = false
theZone.seqRunning = false
theZone.seqIndex = 1 -- we start at one
theZone.intervalIndex = 1 -- here too
theZone.seqStarted = false
if theZone.onStart then
theZone.continue(theZone)
end
end
---
--- update
---
function sequencer.update()
-- call me in a second to poll triggers
local now = timer.getTime()
timer.scheduleFunction(sequencer.update, {}, now + 1)
for idx, theZone in pairs(sequencer.sequencers) do
-- see if reset was pulled
if theZone.resetSeq and cfxZones.testZoneFlag(theZone, theZone.resetSeq, theZone.seqTriggerMethod, "lastResetSeq") then
sequencer.reset(theZone)
end
--trigger.action.outText("have as " .. theZone.startSeq, 30)
-- first, check if we need to pause or continue
if (not theZone.seqRunning) and theZone.startSeq and
cfxZones.testZoneFlag(theZone, theZone.startSeq, theZone.seqTriggerMethod, "lastStartSeq") then
sequencer.continue(theZone)
if theZone.verbose or sequencer.verbose then
trigger.action.outText("+++seq: continuing sequencer <" .. theZone.name .. ">", 30)
end
else
-- synch the start flag so we don't immediately trigger
-- when it starts
if theZone.startSeq then
theZone.lastStartSeq = cfxZones.getFlagValue(theZone.startSeq, theZone)
end
end
if theZone.seqRunning and theZone.stopSeq and
cfxZones.testZoneFlag(theZone, theZone.stopSeq, theZone.seqTriggerMethod, "lastStopSeq") then
sequencer.pause(theZone)
if theZone.verbose or sequencer.verbose then
trigger.action.outText("+++seq: pausing sequencer <" .. theZone.name .. ">", 30)
end
else
if theZone.stopSeq then
theZone.lastStopSeq = cfxZones.getFlagValue(theZone.stopSeq, theZone)
end
end
-- if we are running, see if we timed out
if theZone.seqRunning then
-- check if we have received a 'next' signal
local doNext = false
if theZone.nextSeq then
doNext = cfxZones.testZoneFlag(theZone, theZone.nextSeq, theZone.seqTriggerMethod, "lastNextSeq")
if doNext and (sequencer.verbose or theZone.verbose) then
trigger.action.outText("+++seq: 'next' command received for sequencer <" .. theZone.name .. "> on <" .. theZone.nextSeq .. ">", 30)
end
end
-- check if we are over time limit
if doNext or (theZone.timeLimit < now) then
-- we are timed out or triggered!
if theZone.nextSeq then
theZone.lastNextSeq = cfxZones.getFlagValue(theZone.nextSeq, theZone)
end
sequencer.fire(theZone)
sequencer.advanceInterval(theZone)
if sequencer.advanceSeq(theZone) then
-- start next round
sequencer.startWaitCycle(theZone)
else
if theZone.seqDone then
cfxZones.pollFlag(theZone.seqDone, theZone.seqMethod, theZone)
if theZone.verbose or sequencer.verbose then
trigger.action.outText("+++seq: banging done! flag <" .. theZone.seqDone .. "> for sequence <" .. theZone.name .. ">", 30)
end
end
theZone.seqRunning = false
theZone.seqComplete = true -- can't be restarted unless reset
end -- else no advance
end -- if time limit
end -- if running
end -- for all sequencers
end
--
-- start cycle: force all onStart to fire
--
function sequencer.startCycle()
for idx, theZone in pairs(sequencer.sequencers) do
-- a sequence can be already running when persistence
-- loaded a sequencer
if theZone.onStart then
if theZone.seqStarted then
-- suppressed by persistence
else
if sequencer.verbose or theZone.verbose then
trigger.action.outText("+++seq: starting sequencer " .. theZone.name, 30)
end
sequencer.continue(theZone)
end
end
end
end
--
-- LOAD / SAVE
--
function sequencer.saveData()
local theData = {}
local allSequencers = {}
local now = timer.getTime()
for idx, theSeq in pairs(sequencer.sequencers) do
local theName = theSeq.name
local seqData = {}
seqData.seqComplete = theSeq.seqComplete
seqData.seqRunning = theSeq.seqRunning
seqData.seqIndex = theSeq.seqIndex
seqData.intervalIndex = theSeq.intervalIndex
seqData.seqStarted = theSeq.seqStarted
seqData.timeRemaining = theSeq.timeRemaining
if theSeq.seqRunning then
seqData.timeRemaining = theSeq.timeLimit - now
end
allSequencers[theName] = seqData
end
theData.allSequencers = allSequencers
return theData
end
function sequencer.loadData()
if not persistence then return end
local theData = persistence.getSavedDataForModule("sequencer")
if not theData then
if sequencer.verbose then
trigger.action.outText("+++seq Persistence: no save date received, skipping.", 30)
end
return
end
local allSequencers = theData.allSequencers
if not allSequencers then
if sequencer.verbose then
trigger.action.outText("+++seq Persistence: no sequencer data, skipping", 30)
end
return
end
local now = timer.getTime()
for theName, seqData in pairs(allSequencers) do
local theSeq = sequencer.getSequenceByName(theName)
if theSeq then
theSeq.seqComplete = seqData.seqComplete
theSeq.seqIndex = seqData.seqIndex
theSeq.intervalIndex = seqData.intervalIndex
theSeq.seqStarted = seqData.seqStarted
theSeq.seqRunning = seqData.seqRunning
theSeq.timeRemaining = seqData.timeRemaining
if theSeq.seqRunning then
theSeq.timeLimit = now + theSeq.timeRemaining
end
else
trigger.action.outText("+++seq: persistence: cannot synch sequencer <" .. theName .. ">, skipping", 40)
end
end
end
--
-- start module and read config
--
function sequencer.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("sequencerConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("sequencerConfig")
if sequencer.verbose then
trigger.action.outText("***RND: NO config zone!", 30)
end
end
sequencer.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
if sequencer.verbose then
trigger.action.outText("***RND: read config", 30)
end
end
function sequencer.start()
-- lib check
if not dcsCommon then
trigger.action.outText("sequencer requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("cfx Sequencer",
sequencer.requiredLibs) then
return false
end
-- read config
sequencer.readConfigZone()
-- process RND Zones
local attrZones = cfxZones.getZonesWithAttributeNamed("sequence!")
if sequencer.verbose then
local a = dcsCommon.getSizeOfTable(attrZones)
trigger.action.outText("sequencers: " .. a, 30)
end
-- now create an rnd gen for each one and add them
-- to our watchlist
for k, aZone in pairs(attrZones) do
sequencer.createSequenceWithZone(aZone) -- process attribute and add to zone
sequencer.addSequencer(aZone) -- remember it so we can smoke it
end
-- persistence
if persistence then
-- sign up for persistence
callbacks = {}
callbacks.persistData = sequencer.saveData
persistence.registerModule("sequencer", callbacks)
-- now load my data
sequencer.loadData()
end
-- schedule start cycle
timer.scheduleFunction(sequencer.startCycle, {}, timer.getTime() + 0.25)
-- start update
timer.scheduleFunction(sequencer.update, {}, timer.getTime() + 1)
trigger.action.outText("cfx Sequencer v" .. sequencer.version .. " started.", 30)
return true
end
-- let's go!
if not sequencer.start() then
trigger.action.outText("cf/x Sequencer aborted: missing libraries", 30)
sequencer = nil
end
--[[--
to do:
- currSeq always returns current sequence number
- timeLeft returns current time limit in seconds
--]]--