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

View File

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

View File

@ -1,5 +1,5 @@
cfxReconMode = {}
cfxReconMode.version = "2.1.1"
cfxReconMode.version = "2.1.2"
cfxReconMode.verbose = false -- set to true for debug info
cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered
@ -79,6 +79,9 @@ VERSION HISTORY
- activate / deactivate by flags
2.1.1 - Lat Lon and MGRS also give Elevation
- 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
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 currPoint = theUnit:getPoint()
local ele = math.floor(land.getHeight({x = currPoint.x, y = currPoint.z}))
local units = "m"
if cfxReconMode.imperialUnits then
ele = math.floor(ele * 3.28084) -- feet
units = "ft"
else
ele = math.floor(ele) -- meters
end
if cfxReconMode.mgrs then
local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint))
msg = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing .. " Ele " .. ele .."m"
msg = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing .. " Ele " .. ele .. units
else
local lat, lon, alt = coord.LOtoLL(currPoint)
lat, lon = dcsCommon.latLon2Text(lat, lon)
msg = "Lat " .. lat .. " Lon " .. lon .. " Ele " .. ele .."m"
msg = "Lat " .. lat .. " Lon " .. lon .. " Ele " .. ele ..units
end
return msg
end
@ -483,12 +494,21 @@ function cfxReconMode.processZoneMessage(inMsg, theZone, theGroup)
local theUnit = dcsCommon.getFirstLivingUnit(theGroup)
currPoint = theUnit:getPoint()
end
local ele = math.floor(land.getHeight({x = currPoint.x, y = currPoint.z}))
local units = "m"
if cfxReconMode.imperialUnits then
ele = math.floor(ele * 3.28084) -- feet
units = "ft"
else
ele = math.floor(ele) -- meters
end
local lat, lon, alt = coord.LOtoLL(currPoint)
lat, lon = dcsCommon.latLon2Text(lat, lon)
outMsg = outMsg:gsub("<lat>", lat)
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 mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
outMsg = outMsg:gsub("<mgrs>", mgrs)
@ -1019,6 +1039,10 @@ function cfxReconMode.readConfigZone()
cfxReconMode.lastDeActivate = cfxZones.getFlagValue(cfxReconMode.deactivate, theZone)
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
end

View File

@ -1,5 +1,5 @@
cloneZones = {}
cloneZones.version = "1.5.2"
cloneZones.version = "1.5.4"
cloneZones.verbose = false
cloneZones.requiredLibs = {
"dcsCommon", -- always
@ -59,6 +59,8 @@ cloneZones.allCObjects = {} -- all clones objects
1.5.0 - persistence
1.5.1 - fixed static data cloning bug (load & save)
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
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)
end
@ -292,6 +294,11 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner"
if cfxZones.hasProperty(theZone, "rndLoc") then
theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "rndLoc", false)
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.onRoad = cfxZones.getBoolFromZoneProperty(theZone, "onRoad", false)
@ -308,13 +315,16 @@ end
--
function cloneZones.despawnAll(theZone)
if cloneZones.verbose then
trigger.action.outText("wiping <" .. theZone.name .. ">", 30)
if cloneZones.verbose or theZone.verbose then
trigger.action.outText("+++clnZ: despawn all - wiping zone <" .. theZone.name .. ">", 30)
end
for idx, aGroup in pairs(theZone.mySpawns) do
--trigger.action.outText("++clnZ: despawn all " .. aGroup.name, 30)
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)
Group.destroy(aGroup)
end
@ -323,7 +333,7 @@ function cloneZones.despawnAll(theZone)
-- warning! may be mismatch because we are looking at groups
-- not objects. let's see
if aStatic:isExist() then
if cloneZones.verbose then
if cloneZones.verbose or theZone.verbose then
trigger.action.outText("Destroying static <" .. aStatic:getName() .. ">", 30)
end
cloneZones.invokeCallbacks(theZone, "will despawn static", aStatic)
@ -334,12 +344,47 @@ function cloneZones.despawnAll(theZone)
theZone.myStatics = {}
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!!
theData.x = theData.x + zoneDelta.x
theData.y = theData.y + zoneDelta.z -- !!!
local units = theData.units
local departFromAerodrome = false
--local departingAerodrome
local fromParking = false
for idx, aUnit in pairs(units) do
aUnit.x = aUnit.x + zoneDelta.x
aUnit.y = aUnit.y + zoneDelta.z -- again!!!!
@ -373,8 +418,11 @@ function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWayp
loc.y = 0
loc.z = firstPoint.y
local bestAirbase = dcsCommon.getClosestAirbaseTo(loc)
--departingAerodrome = bestAirbase
firstPoint.airdromeId = bestAirbase:getID()
-- trigger.action.outText("first: adjusted to " .. firstPoint.airdromeId, 30)
departFromAerodrome = true
fromParking = dcsCommon.stringStartsWith(firstPoint.action, "From Parking")
end
-- adjust last point (landing)
@ -393,8 +441,20 @@ function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWayp
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
function cloneZones.uniqueID()
local uid = cloneZones.uniqueCounter
cloneZones.uniqueCounter = cloneZones.uniqueCounter + 1
@ -670,6 +730,9 @@ function cloneZones.handoffTracking(theGroup, theZone)
end
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
-- spawnZone is the spawner with settings
-- if not spawnZone then spawnZone = theZone end
@ -697,17 +760,34 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
local theCat = cfxMX.catText2ID(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)
-- apply randomizer if selected
if spawnZone.rndLoc then
--trigger.action.outText("rndloc for <" .. spawnZone.name .. ">", 30)
-- calculate the entire group's displacement
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
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)
if not spawnZone.centerOnly then
-- *every unit's displacement is randomized
r = math.random() * spawnZone.radius
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.y = aUnit.y + dy
end
@ -715,44 +795,71 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
if spawnZone.rndHeading then
local units = rawData.units
for idx, aUnit in pairs(units) do
local phi = 6.2831 * math.random() -- that's 2Pi, folx
aUnit.heading = phi
if spawnZone.centerOnly and units and units[1] then
-- rotate entire group around unit 1
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
-- apply onRoad option if selected
if spawnZone.onRoad then
local units = rawData.units
local iterCount = 0
local otherLocs = {} -- resolved locs
for idx, aUnit in pairs(units) do
local cx = aUnit.x
local cy = aUnit.y
-- we now iterate until there is enough separation or too many iters
local tooClose
local np, nx, ny
repeat
nx, ny = land.getClosestPointOnRoads("roads", cx, cy)
-- 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
if spawnZone.centerOnly then
-- only place the first unit in group on roads
-- and displace all other with the same offset
local hasOffset = false
local dx, dy, cx, cy
for idx, aUnit in pairs(units) do
cx = aUnit.x
cy = aUnit.y
if not hasOffset then
local nx, ny = land.getClosestPointOnRoads("roads", cx, cy)
dx = nx - cx
dy = ny - cy
hasOffset = true
end
aUnit.x = cx + dx
aUnit.y = cy + dy
end
else
local iterCount = 0
local otherLocs = {} -- resolved locs
for idx, aUnit in pairs(units) do
local cx = aUnit.x
local cy = aUnit.y
-- we now iterate until there is enough separation or too many iters
local tooClose
local np, nx, ny
repeat
nx, ny = land.getClosestPointOnRoads("roads", cx, cy)
-- 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
@ -946,6 +1053,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
end
function cloneZones.spawnWithCloner(theZone)
trigger.action.outText("+++clnZ: enter spawnWithCloner for <" .. theZone.name .. ">", 30)
if not theZone then
trigger.action.outText("+++clnZ: nil zone on spawnWithCloner", 30)
return
@ -966,14 +1074,17 @@ function cloneZones.spawnWithCloner(theZone)
local templates = dcsCommon.splitString(templateName, ",")
templateName = dcsCommon.pickRandom(templates)
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)
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)
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)
end
return
@ -1118,10 +1229,10 @@ function cloneZones.update()
end
end
function cloneZones.onStart()
--trigger.action.outText("+++clnZ: Enter atStart", 30)
function cloneZones.doOnStart()
for idx, theZone in pairs(cloneZones.cloners) do
if theZone.onStart then
trigger.action.outText("+++clnZ: onStart true for <" .. theZone.name .. ">", 30)
if theZone.isStarted then
if cloneZones.verbose or theZone.verbose then
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
-- persistencey has loaded isStarted if a cloner was
-- already started
timer.scheduleFunction(cloneZones.onStart, {}, timer.getTime() + 0.1)
timer.scheduleFunction(cloneZones.doOnStart, {}, timer.getTime() + 1.0)
-- start update
cloneZones.update()

View File

@ -1,5 +1,5 @@
dcsCommon = {}
dcsCommon.version = "2.7.1"
dcsCommon.version = "2.7.2"
--[[-- VERSION HISTORY
2.2.6 - compassPositionOfARelativeToB
- clockPositionOfARelativeToB
@ -52,7 +52,7 @@ dcsCommon.version = "2.7.1"
- dcsCommon.trimArray(
- createStaticObjectData uses trim for type
- 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.1 - vNorm(a)
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
new getAllExistingPlayerUnitsRaw - from cfxPlayer
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
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
--
@ -1823,6 +1887,7 @@ end
end
function dcsCommon.stringStartsWith(theString, thePrefix)
if not theString then return false end
return theString:find(thePrefix) == 1
end
@ -2473,6 +2538,7 @@ function dcsCommon.flagArrayFromString(inString, verbose)
local rawElements = dcsCommon.splitString(inString, ",")
-- go over all elements
for idx, anElement in pairs(rawElements) do
anElement = dcsCommon.trim(anElement)
if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then
-- interpret this as a range
local theRange = dcsCommon.splitString(anElement, "-")
@ -2498,7 +2564,7 @@ function dcsCommon.flagArrayFromString(inString, verbose)
end
else
-- 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
table.insert(flags, f)
@ -2513,6 +2579,81 @@ function dcsCommon.flagArrayFromString(inString, verbose)
return flags
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)
table.insert(theCollector, theObject)
return true

View File

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

View File

@ -1,5 +1,5 @@
messenger = {}
messenger.version = "1.3.3"
messenger.version = "2.0.0"
messenger.verbose = false
messenger.requiredLibs = {
"dcsCommon", -- always
@ -26,6 +26,22 @@ messenger.messengers = {}
- can interpret <lat>, <lon>, <mgrs>
- zone-local verbosity
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
--
function messenger.preProcMessage(inMsg, theZone)
-- Replace STATIC bits of message like CR and zone name
if not inMsg then return "<nil inMsg>" end
local formerType = type(inMsg)
if formerType ~= "string" then inMsg = tostring(inMsg) end
@ -58,27 +75,172 @@ function messenger.preProcMessage(inMsg, theZone)
if theZone then
outMsg = outMsg:gsub("<z>", theZone.name)
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
local absSecs = timer.getAbsTime()-- + env.mission.start_time
while absSecs > 86400 do
absSecs = absSecs - 86400 -- subtract out all days
end
local timeString = dcsCommon.processHMS("<:h>:<:m>:<:s>", absSecs)
outMsg = outMsg:gsub("<t>", timeString)
local timeString = dcsCommon.processHMS(theZone.msgTimeFormat, absSecs)
local outMsg = inMsg:gsub("<t>", timeString)
-- replace <lat> with lat of zone point and <lon> with lon of zone point
-- and <mgrs> with mgrs coords of zone point
local currPoint = cfxZones.getPoint(theZone)
local lat, lon, alt = coord.LOtoLL(currPoint)
local lat, lon = coord.LOtoLL(currPoint)
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("<lon>", lon)
outMsg = outMsg:gsub("<ele>", alt)
local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint))
local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
outMsg = outMsg:gsub("<mgrs>", mgrs)
return outMsg
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)
-- start val - a range
@ -147,6 +309,7 @@ function messenger.createMessengerWithZone(theZone)
theZone.lastMessageOn = cfxZones.getFlagValue(theZone.messageOnFlag, theZone)
end
-- reveiver: coalition, group, unit
if cfxZones.hasProperty(theZone, "coalition") then
theZone.msgCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0)
end
@ -155,11 +318,45 @@ function messenger.createMessengerWithZone(theZone)
theZone.msgCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "msgCoalition", 0)
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
theZone.messageValue = cfxZones.getStringFromZoneProperty(theZone, "messageValue?", "<none>")
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
trigger.action.outText("+++Msg: new zone <".. theZone.name .."> will say <".. theZone.message .. ">", 30)
end
@ -182,12 +379,22 @@ function messenger.getMessage(theZone)
-- replace *zone and *value wildcards
msg = string.gsub(msg, "*name", zName)
msg = string.gsub(msg, "*value", zVal)
--msg = string.gsub(msg, "*name", zName)-- deprecated
--msg = string.gsub(msg, "*value", zVal) -- deprecated
-- old-school <v> to provide value from messageValue
msg = string.gsub(msg, "<v>", zVal)
local z = tonumber(zVal)
if not z then z = 0 end
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
end
@ -212,6 +419,20 @@ function messenger.isTriggered(theZone)
if theZone.msgCoalition then
trigger.action.outTextForCoalition(theZone.msgCoalition, msg, theZone.duration, theZone.clearScreen)
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
-- out to all
trigger.action.outText(msg, theZone.duration, theZone.clearScreen)
@ -312,5 +533,10 @@ end
--[[--
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.version = "1.1.0"
radioMenu.version = "2.0.0"
radioMenu.verbose = false
radioMenu.ups = 1
radioMenu.requiredLibs = {
@ -15,8 +15,18 @@ radioMenu.menus = {}
1.1.0 removeMenu
addMenu
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)
table.insert(radioMenu.menus, theZone)
end
@ -35,44 +45,192 @@ end
--
-- read zone
--
function radioMenu.installMenu(theZone)
if theZone.coalition == 0 then
theZone.rootMenu = missionCommands.addSubMenu(theZone.rootName, nil)
function radioMenu.filterPlayerIDForType(theZone)
-- note: we currently ignore coalition
local theIDs = {}
local allTypes = {}
if dcsCommon.containsString(theZone.menuTypes, ",") then
allTypes = dcsCommon.splitString(theZone.menuTypes, ",")
else
theZone.rootMenu = missionCommands.addSubMenuForCoalition(theZone.coalition, theZone.rootName, nil)
table.insert(allTypes, theZone.menuTypes)
end
local menuA = cfxZones.getStringFromZoneProperty(theZone, "itemA", "<no A submenu>")
if theZone.coalition == 0 then
theZone.menuA = missionCommands.addCommand(menuA, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "A"})
-- now iterate all types, and include any player that matches
-- note that a player may match twice, so we use a dict instead of an
-- 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
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
if cfxZones.hasProperty(theZone, "itemB") then
local menuB = cfxZones.getStringFromZoneProperty(theZone, "itemB", "<no B submenu>")
if theZone.coalition == 0 then
theZone.menuB = missionCommands.addCommand(menuB, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "B"})
if theZone.menuGroup or theZone.menuTypes then
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
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
if cfxZones.hasProperty(theZone, "itemC") then
local menuC = cfxZones.getStringFromZoneProperty(theZone, "itemC", "<no C submenu>")
if theZone.coalition == 0 then
theZone.menuC = missionCommands.addCommand(menuC, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "C"})
if theZone.menuGroup or theZone.menuTypes then
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
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
if cfxZones.hasProperty(theZone, "itemD") then
local menuD = cfxZones.getStringFromZoneProperty(theZone, "itemD", "<no D submenu>")
if theZone.coalition == 0 then
theZone.menuD = missionCommands.addCommand(menuD, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "D"})
if theZone.menuGroup or theZone.menuTypes then
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
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
@ -81,6 +239,18 @@ function radioMenu.createRadioMenuWithZone(theZone)
theZone.rootName = cfxZones.getStringFromZoneProperty(theZone, "radioMenu", "<No Name>")
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)
@ -99,22 +269,22 @@ function radioMenu.createRadioMenuWithZone(theZone)
theZone.itemAChosen = cfxZones.getStringFromZoneProperty(theZone, "A!", "*<none>")
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.itemBChosen = cfxZones.getStringFromZoneProperty(theZone, "B!", "*<none>")
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.itemCChosen = cfxZones.getStringFromZoneProperty(theZone, "C!", "*<none>")
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.itemDChosen = cfxZones.getStringFromZoneProperty(theZone, "D!", "*<none>")
theZone.cooldownD = cfxZones.getNumberFromZoneProperty(theZone, "cooldownD", 0)
theZone.mcdD = 0
--theZone.mcdD = 0
theZone.busyD = cfxZones.getStringFromZoneProperty(theZone, "busyD", "Please stand by (<s> seconds)")
if cfxZones.hasProperty(theZone, "removeMenu?") then
@ -160,24 +330,43 @@ function radioMenu.redirectMenuX(args)
timer.scheduleFunction(radioMenu.doMenuX, args, timer.getTime() + 0.1)
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)
theZone = args[1]
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 theFlag = theZone.itemAChosen
-- decode A..X
if theItemIndex == "B"then
cd = theZone.mcdB
cd = radioMenu.cdByGID(theZone.mcdB, theZone, theGroup) -- theZone.mcdB
busy = theZone.busyB
theFlag = theZone.itemBChosen
elseif theItemIndex == "C" then
cd = theZone.mcdC
cd = radioMenu.cdByGID(theZone.mcdC, theZone, theGroup) -- theZone.mcdC
busy = theZone.busyC
theFlag = theZone.itemCChosen
elseif theItemIndex == "D" then
cd = theZone.mcdD
cd = radioMenu.cdByGID(theZone.mcdD, theZone, theGroup) -- theZone.mcdD
busy = theZone.busyD
theFlag = theZone.itemDChosen
end
@ -193,13 +382,13 @@ function radioMenu.doMenuX(args)
-- set new cooldown -- needs own decoder A..X
if theItemIndex == "A" then
theZone.mcdA = now + theZone.cooldownA
radioMenu.setCDByGID("mcdA", theZone, theGroup, now + theZone.cooldownA)
elseif theItemIndex == "B" then
theZone.mcdB = now + theZone.cooldownB
radioMenu.setCDByGID("mcdB", theZone, theGroup, now + theZone.cooldownB)
elseif theItemIndex == "C" then
theZone.mcdC = now + theZone.cooldownC
radioMenu.setCDByGID("mcdC", theZone, theGroup, now + theZone.cooldownC)
else
theZone.mcdD = now + theZone.cooldownD
radioMenu.setCDByGID("mcdC", theZone, theGroup, now + theZone.cooldownC)
end
cfxZones.pollFlag(theFlag, theZone.radioMethod, theZone)
@ -208,10 +397,10 @@ function radioMenu.doMenuX(args)
end
end
--
-- Update -- required when we can enable/disable a zone's menu
--
function radioMenu.update()
-- call me in a second to poll triggers
timer.scheduleFunction(radioMenu.update, {}, timer.getTime() + 1/radioMenu.ups)
@ -221,15 +410,15 @@ function radioMenu.update()
if theZone.removeMenu
and cfxZones.testZoneFlag(theZone, theZone.removeMenu, theZone.radioTriggerMethod, "lastRemoveMenu")
and theZone.menuVisible
then
if theZone.verbose or radioMenu.verbose then
trigger.action.outText("+++menu: removing <" .. dcsCommon.menu2text(theZone.rootMenu) .. "> for <" .. theZone.name .. ">", 30)
end
if theZone.coalition == 0 then
missionCommands.removeItem(theZone.rootMenu)
then
if theZone.menuGroup or theZone.menuTypes then
for gID, aRoot in pairs(theZone.rootMenu) do
missionCommands.removeItemForGroup(gID, aRoot)
end
elseif theZone.coalition == 0 then
missionCommands.removeItem(theZone.rootMenu[0])
else
missionCommands.removeItemForCoalition(theZone.coalition, theZone.rootMenu)
missionCommands.removeItemForCoalition(theZone.coalition, theZone.rootMenu[0])
end
theZone.menuVisible = false
@ -304,5 +493,6 @@ if not radioMenu.start() then
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
--]]--