Version 1.3.1

stopGaps
owned zones
factory zones
flag documentation
This commit is contained in:
Christian Franz 2023-05-24 13:51:57 +02:00
parent 83f9478f86
commit 3d058a12f2
17 changed files with 1219 additions and 220 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
cfxGroundTroops = {} cfxGroundTroops = {}
cfxGroundTroops.version = "1.7.7" cfxGroundTroops.version = "1.7.8"
cfxGroundTroops.ups = 1 cfxGroundTroops.ups = 1
cfxGroundTroops.verbose = false cfxGroundTroops.verbose = false
cfxGroundTroops.requiredLibs = { cfxGroundTroops.requiredLibs = {
@ -67,7 +67,7 @@ cfxGroundTroops.deployedTroops = {} -- indexed by group name
- makeTroopsEngageZone() sets 'moving' status to true - makeTroopsEngageZone() sets 'moving' status to true
- createGroundTroops() sets moving status to false - createGroundTroops() sets moving status to false
- updateZoneAttackers() uses moving - updateZoneAttackers() uses moving
1.7.8 - better guards before invoking ownedZones
an entry into the deployed troop table has the following attributes an entry into the deployed troop table has the following attributes
- group - the group - group - the group
@ -296,6 +296,10 @@ end
function cfxGroundTroops.updateZoneAttackers(troop) function cfxGroundTroops.updateZoneAttackers(troop)
if not troop then return end if not troop then return end
if not cfxOwnedZones then
trigger.action.outText("+++gndT: update zone attackers requires ownedZones", 30)
return
end
troop.insideDestination = false -- mark as not inside troop.insideDestination = false -- mark as not inside
local newTargetZone = cfxGroundTroops.getClosestEnemyZone(troop) local newTargetZone = cfxGroundTroops.getClosestEnemyZone(troop)

View File

@ -32,7 +32,7 @@ cfxMX.unitIDbyName = {}
cfxMX.groupDataByName = {} cfxMX.groupDataByName = {}
cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"... cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"...
cfxMX.groupCoalitionByName = {} cfxMX.groupCoalitionByName = {}
cfxMX.countryByName ={} cfxMX.countryByName ={} -- county of group named
cfxMX.linkByName = {} cfxMX.linkByName = {}
cfxMX.allFixedByName = {} cfxMX.allFixedByName = {}
cfxMX.allHeloByName = {} cfxMX.allHeloByName = {}

View File

@ -6,20 +6,33 @@ cfxOwnedZones.name = "cfxOwnedZones"
--[[-- VERSION HISTORY --[[-- VERSION HISTORY
2.0.0 - factored from cfxOwnedZones 1.x, separating out production 2.0.0 - factored from cfxOwnedZones 1.x, separating out production
- moved to flag# semantic
- xxxOwned# for all
- ownedBy# supports multFlag
- xxxOwned#
- redLine, blueLine
- redFill, blueFill
- neutralLine, neutralFill
- global and per-zone colors
- auto-defaulting colors from config
- supports poly zone
- groundCap option
- navalCap option
- heloCap option
- fixWingCap option
- filter water owned zones for groundTroops
--]]-- --]]--
cfxOwnedZones.requiredLibs = { cfxOwnedZones.requiredLibs = {
"dcsCommon", -- common is of course needed for everything "dcsCommon",
-- pretty stupid to check for this since we "cfxZones",
-- need common to invoke the check, but anyway
"cfxZones", -- Zones, of course
} }
cfxOwnedZones.zones = {} cfxOwnedZones.zones = {}
cfxOwnedZones.ups = 1 cfxOwnedZones.ups = 1
cfxOwnedZones.initialized = false cfxOwnedZones.initialized = false
--[[-- --[[--
owned zones is a module that managers conquerable zones and keeps a record owned zones is a module that manages conquerable zones and keeps a record
of who owns the zone based on rules of who owns the zone based on rules
@ -83,23 +96,28 @@ function cfxOwnedZones.drawZoneInMap(aZone)
end end
if aZone.hidden then return end if aZone.hidden then return end
local lineColor = {1.0, 0, 0, 1.0} -- red local lineColor = aZone.redLine -- {1.0, 0, 0, 1.0} -- red
local fillColor = {1.0, 0, 0, 0.2} -- red local fillColor = aZone.redFill -- {1.0, 0, 0, 0.2} -- red
local owner = aZone.owner -- cfxOwnedZones.getOwnerForZone(aZone) local owner = aZone.owner -- cfxOwnedZones.getOwnerForZone(aZone)
if owner == 2 then if owner == 2 then
lineColor = {0.0, 0, 1.0, 1.0} lineColor = aZone.blueLine -- {0.0, 0, 1.0, 1.0}
fillColor = {0.0, 0, 1.0, 0.2} fillColor = aZone.blueFill -- {0.0, 0, 1.0, 0.2}
elseif owner == 0 then elseif owner == 0 then
lineColor = {0.8, 0.8, 0.8, 1.0} lineColor = aZone.neutralLine -- {0.8, 0.8, 0.8, 1.0}
fillColor = {0.8, 0.8, 0.8, 0.2} fillColor = aZone.neutralFill -- {0.8, 0.8, 0.8, 0.2}
end end
local theShape = 2 -- circle -- local theShape = 2 -- circle
local markID = dcsCommon.numberUUID() local markID = dcsCommon.numberUUID()
trigger.action.circleToAll(-1, markID, aZone.point, aZone.radius, lineColor, fillColor, 1, true, "") if aZone.isCircle then
aZone.markID = markID trigger.action.circleToAll(-1, markID, aZone.point, aZone.radius, lineColor, fillColor, 1, true, "")
else
local poly = aZone.poly
trigger.action.quadToAll(-1, markID, poly[4], poly[3], poly[2], poly[1], lineColor, fillColor, 1, true, "") -- note: left winding to get fill color
end
aZone.markID = markID
end end
function cfxOwnedZones.getOwnedZoneByName(zName) function cfxOwnedZones.getOwnedZoneByName(zName)
@ -110,7 +128,7 @@ function cfxOwnedZones.getOwnedZoneByName(zName)
end end
function cfxOwnedZones.addOwnedZone(aZone) function cfxOwnedZones.addOwnedZone(aZone)
local owner = aZone.owner --cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) -- is already readm read it again local owner = aZone.owner --cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) -- is already read
if cfxZones.hasProperty(aZone, "conquered!") then if cfxZones.hasProperty(aZone, "conquered!") then
aZone.conqueredFlag = cfxZones.getStringFromZoneProperty(aZone, "conquered!", "*<cfxnone>") aZone.conqueredFlag = cfxZones.getStringFromZoneProperty(aZone, "conquered!", "*<cfxnone>")
@ -130,18 +148,25 @@ function cfxOwnedZones.addOwnedZone(aZone)
if cfxZones.hasProperty(aZone, "neutral!") then if cfxZones.hasProperty(aZone, "neutral!") then
aZone.neutralCap = cfxZones.getStringFromZoneProperty(aZone, "neutral!", "none") aZone.neutralCap = cfxZones.getStringFromZoneProperty(aZone, "neutral!", "none")
end end
if cfxZones.hasProperty(aZone, "ownedBy") then if cfxZones.hasProperty(aZone, "ownedBy#") then
aZone.ownedBy = cfxZones.getStringFromZoneProperty(aZone, "ownedBy#", "none")
elseif cfxZones.hasProperty(aZone, "ownedBy") then
aZone.ownedBy = cfxZones.getStringFromZoneProperty(aZone, "ownedBy", "none") aZone.ownedBy = cfxZones.getStringFromZoneProperty(aZone, "ownedBy", "none")
end end
aZone.ownedTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change")
if cfxZones.hasProperty(aZone, "ownedTriggerMethod") then
aZone.ownedTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "ownedTriggerMethod", "change")
end
aZone.unbeatable = cfxZones.getBoolFromZoneProperty(aZone, "unbeatable", false) aZone.unbeatable = cfxZones.getBoolFromZoneProperty(aZone, "unbeatable", false)
aZone.untargetable = cfxZones.getBoolFromZoneProperty(aZone, "untargetable", false)
aZone.hidden = cfxZones.getBoolFromZoneProperty(aZone, "hidden", false) aZone.hidden = cfxZones.getBoolFromZoneProperty(aZone, "hidden", false)
-- individual colors, else default from config
aZone.redLine = cfxZones.getRGBAVectorFromZoneProperty(aZone, "redLine", cfxOwnedZones.redLine)
aZone.redFill = cfxZones.getRGBAVectorFromZoneProperty(aZone, "redFill", cfxOwnedZones.redFill)
aZone.blueLine = cfxZones.getRGBAVectorFromZoneProperty(aZone, "blueLine", cfxOwnedZones.blueLine)
aZone.blueFill = cfxZones.getRGBAVectorFromZoneProperty(aZone, "blueFill", cfxOwnedZones.blueFill)
aZone.neutralLine = cfxZones.getRGBAVectorFromZoneProperty(aZone, "neutralLine", cfxOwnedZones.neutralLine)
aZone.neutralFill = cfxZones.getRGBAVectorFromZoneProperty(aZone, "neutralFill", cfxOwnedZones.neutralFill)
cfxOwnedZones.zones[aZone] = aZone cfxOwnedZones.zones[aZone] = aZone
cfxOwnedZones.drawZoneInMap(aZone) cfxOwnedZones.drawZoneInMap(aZone)
end end
@ -246,24 +271,54 @@ function cfxOwnedZones.update()
-- new. unit counting update -- new. unit counting update
cfxOwnedZones.updateSchedule = timer.scheduleFunction(cfxOwnedZones.update, {}, timer.getTime() + 1/cfxOwnedZones.ups) cfxOwnedZones.updateSchedule = timer.scheduleFunction(cfxOwnedZones.update, {}, timer.getTime() + 1/cfxOwnedZones.ups)
-- iterate all groups and their units to count how many -- iterate all groups and their units to count how many
-- units are in each zone -- units are in each zone, also count how many zones each side has
local totalZoneNum = 0
local blueZoneNum = 0
local redZoneNum = 0
local greyZoneNum = 0
-- assemble all units in allRed and allBlue according to
-- cap options (boots, ships, rotors, wings)
local allRed = {}
if cfxOwnedZones.groundCap then allRed = coalition.getGroups(1, Group.Category.GROUND) end
if cfxOwnedZones.navalCap then
allRed = dcsCommon.combineTables(allRed, coalition.getGroups(1, Group.Category.SHIP))
end
if cfxOwnedZones.heloCap then
allRed = dcsCommon.combineTables(allRed, coalition.getGroups(1, Group.Category.HELICOPTER))
end
if cfxOwnedZones.fixWingCap then
allRed = dcsCommon.combineTables(allRed, coalition.getGroups(1, Group.Category.AIRPLANE))
end
local allBlue = {}
if cfxOwnedZones.groundCap then allBlue = coalition.getGroups(2, Group.Category.GROUND) end
if cfxOwnedZones.navalCap then
allBlue = dcsCommon.combineTables(allBlue, coalition.getGroups(2, Group.Category.SHIP))
end
if cfxOwnedZones.heloCap then
allBlue = dcsCommon.combineTables(allBlue, coalition.getGroups(2, Group.Category.HELICOPTER))
end
if cfxOwnedZones.fixWingCap then
allBlue = dcsCommon.combineTables(allBlue, coalition.getGroups(2, Group.Category.AIRPLANE))
end
for idz, theZone in pairs(cfxOwnedZones.zones) do for idz, theZone in pairs(cfxOwnedZones.zones) do
theZone.numRed = 0 theZone.numRed = 0
theZone.numBlue = 0 theZone.numBlue = 0
-- count red units -- count red units in zone
local allRed = coalition.getGroups(1, Group.Category.GROUND)
for idx, aGroup in pairs(allRed) do for idx, aGroup in pairs(allRed) do
if Group.isExist(aGroup) then if Group.isExist(aGroup) then
if cfxOwnedZones.fastEval then if cfxOwnedZones.fastEval then
-- we only check first unit that is alive -- we only check first unit that is alive
local theUnit = dcsCommon.getGroupUnit(aGroup) local theUnit = dcsCommon.getGroupUnit(aGroup)
if theUnit and cfxZones.unitInZone(theUnit, theZone) then if theUnit and (not theUnit:inAir()) and cfxZones.unitInZone(theUnit, theZone) then
theZone.numRed = theZone.numRed + aGroup:getSize() theZone.numRed = theZone.numRed + aGroup:getSize()
end end
else else
local allUnits = aGroup:getUnits() local allUnits = aGroup:getUnits()
for idy, theUnit in pairs(allUnits) do for idy, theUnit in pairs(allUnits) do
if cfxZones.unitInZone(theUnit, theZone) then if (not theUnit:inAir()) and cfxZones.unitInZone(theUnit, theZone) then
theZone.numRed = theZone.numRed + 1 theZone.numRed = theZone.numRed + 1
end end
end end
@ -271,19 +326,18 @@ function cfxOwnedZones.update()
end end
end end
-- count blue units -- count blue units
local allBlue = coalition.getGroups(2, Group.Category.GROUND)
for idx, aGroup in pairs(allBlue) do for idx, aGroup in pairs(allBlue) do
if Group.isExist(aGroup) then if Group.isExist(aGroup) then
if cfxOwnedZones.fastEval then if cfxOwnedZones.fastEval then
-- we only check first unit that is alive -- we only check first unit that is alive
local theUnit = dcsCommon.getGroupUnit(aGroup) local theUnit = dcsCommon.getGroupUnit(aGroup)
if theUnit and cfxZones.unitInZone(theUnit, theZone) then if theUnit and (not theUnit:inAir()) and cfxZones.unitInZone(theUnit, theZone) then
theZone.numBlue = theZone.numBlue + aGroup:getSize() theZone.numBlue = theZone.numBlue + aGroup:getSize()
end end
else else
local allUnits = aGroup:getUnits() local allUnits = aGroup:getUnits()
for idy, theUnit in pairs(allUnits) do for idy, theUnit in pairs(allUnits) do
if cfxZones.unitInZone(theUnit, theZone) then if (not theUnit:inAir()) and cfxZones.unitInZone(theUnit, theZone) then
theZone.numBlue = theZone.numBlue + 1 theZone.numBlue = theZone.numBlue + 1
end end
end end
@ -368,11 +422,51 @@ function cfxOwnedZones.update()
-- update ownership flag if exists -- update ownership flag if exists
if theZone.ownedBy then if theZone.ownedBy then
cfxZones.setFlagValue(theZone.ownedBy, theZone.owner, theZone) cfxZones.setFlagValueMult(theZone.ownedBy, theZone.owner, theZone)
end
-- now add this zone to relevant side
totalZoneNum = totalZoneNum + 1
if newOwner == 0 then
greyZoneNum = greyZoneNum + 1
elseif newOwner == 1 then
redZoneNum = redZoneNum + 1
else
blueZoneNum = blueZoneNum + 1
end end
end -- iterating all zones end -- iterating all zones
-- update totals
if cfxOwnedZones.redOwned then
cfxZones.setFlagValueMult(cfxOwnedZones.redOwned, redZoneNum, cfxOwnedZones)
end
if cfxOwnedZones.blueOwned then
cfxZones.setFlagValueMult(cfxOwnedZones.blueOwned, blueZoneNum, cfxOwnedZones)
end
if cfxOwnedZones.neutralOwned then
cfxZones.setFlagValueMult(cfxOwnedZones.neutralOwned, greyZoneNum, cfxOwnedZones)
end
if cfxOwnedZones.totalOwnedZones then
cfxZones.setFlagValueMult(cfxOwnedZones.totalOwnedZones, totalZoneNum, cfxOwnedZones)
end
-- see if one side owns all and bang the flags if requiredLibs
if cfxOwnedZones.allBlue and not cfxOwnedZones.hasAllBlue then
if cfxOwnedZones.sideOwnsAll(2) then
cfxZones.pollFlag(cfxOwnedZones.allBlue, "inc", cfxOwnedZones)
cfxOwnedZones.hasAllBlue = true
end
end
if cfxOwnedZones.allRed and not cfxOwnedZones.hasAllRed then
if cfxOwnedZones.sideOwnsAll(1) then
cfxZones.pollFlag(cfxOwnedZones.allRed, "inc", cfxOwnedZones)
cfxOwnedZones.hasAllRed = true
end
end
end end
function cfxOwnedZones.sideOwnsAll(theSide) function cfxOwnedZones.sideOwnsAll(theSide)
@ -393,6 +487,162 @@ function cfxOwnedZones.hasOwnedZones()
return false return false
end end
-- getting closest owned zones etc
-- required for groundTroops and factory attackers
-- methods provided only for other modules (e.g. cfxGroundTroops or
-- factoryZone
--
-- collect zones can filter owned zones.
-- by default it filters all zones that are in water
function cfxOwnedZones.collectZones(mode)
if not mode then mode = "land" end
if mode == "land" then
local landZones = {}
for idx, theZone in pairs(cfxOwnedZones.zones) do
p = cfxZones.getPoint(theZone)
p.y = p.z
local surfType = land.getSurfaceType(p)
if surfType == 3 then
else
table.insert(landZones, theZone)
end
end
return landZones
end
-- return all zones
return cfxOwnedZones.zones
--if not mode then mode = "OWNED" end
-- Note: since cfxGroundTroops currently simply uses owner flag
-- we cannot migrate to a differentiation between factory and
-- owned. All produced attackers always attack owned zones.
end
function cfxOwnedZones.getEnemyZonesFor(aCoalition)
local enemyZones = {}
local allZones = cfxOwnedZones.collectZones()
local ourEnemy = dcsCommon.getEnemyCoalitionFor(aCoalition)
for zKey, aZone in pairs(allZones) do
if aZone.owner == ourEnemy then -- only check enemy owned zones
-- note: will include untargetable zones
table.insert(enemyZones, aZone)
end
end
return enemyZones
end
function cfxOwnedZones.getNearestOwnedZoneToPoint(aPoint)
local shortestDist = math.huge
local closestZone = nil
local allZones = cfxOwnedZones.collectZones()
for zKey, aZone in pairs(allZones) do
local zPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and
currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestOwnedZone(theZone)
local shortestDist = math.huge
local closestZone = nil
local aPoint = cfxZones.getPoint(theZone)
local allZones = cfxOwnedZones.collectZones()
for zKey, aZone in pairs(allZones) do
local zPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestEnemyOwnedZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
local allZones = cfxOwnedZones.collectZones()
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies
local zPoint = cfxZones.getPoint(theZone)
for zKey, aZone in pairs(allZones) do
if targetNeutral then
-- return all zones that do not belong to us
if aZone.owner ~= theZone.owner then
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(aPoint, zPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- return zones that are taken by the Enenmy
if aZone.owner == ourEnemy then -- only check own zones
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestFriendlyZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies nor friends, all zones would be legal.
local zPoint = cfxZones.getPoint(theZone)
local allZones = cfxOwnedZones.collectZones()
for zKey, aZone in pairs(allZones) do
if targetNeutral then
-- target all zones that do not belong to the enemy
if aZone.owner ~= ourEnemy then
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- only target zones that are taken by us
if aZone.owner == theZone.owner then -- only check own zones
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.enemiesRemaining(aZone)
if cfxOwnedZones.getNearestEnemyOwnedZone(aZone) then return true end
return false
end
-- --
-- load / save data -- load / save data
@ -415,8 +665,6 @@ function cfxOwnedZones.saveData()
allZoneData[theZone.name] = zoneData allZoneData[theZone.name] = zoneData
end end
-- now iterate all attack groups that we have spawned and that
-- (maybe) are still alive
-- now write the info for the flags that we output for #red, etc -- now write the info for the flags that we output for #red, etc
local flagInfo = {} local flagInfo = {}
flagInfo.neutral = cfxZones.getFlagValue(cfxOwnedZones.neutralTriggerFlag, cfxOwnedZones) flagInfo.neutral = cfxZones.getFlagValue(cfxOwnedZones.neutralTriggerFlag, cfxOwnedZones)
@ -478,10 +726,46 @@ function cfxOwnedZones.readConfigZone(theZone)
cfxOwnedZones.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) cfxOwnedZones.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
cfxOwnedZones.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true) cfxOwnedZones.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true)
cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r!", "*<cfxnone>") if cfxZones.hasProperty(theZone, "r!") then
cfxOwnedZones.blueTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b!", "*<cfxnone>") cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r!", "*<cfxnone>")
cfxOwnedZones.neutralTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n!", "*<cfxnone>") else
cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r#", "*<cfxnone>")
end
if cfxZones.hasProperty(theZone, "b!") then
cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b!", "*<cfxnone>")
else
cfxOwnedZones.blueTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b#", "*<cfxnone>")
end
if cfxZones.hasProperty(theZone, "n!") then
cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n!", "*<cfxnone>")
else
cfxOwnedZones.neutralTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n#", "*<cfxnone>")
end
-- allXXX flags
if cfxZones.hasProperty(theZone, "allBlue!") then
cfxOwnedZones.allBlue = cfxZones.getStringFromZoneProperty(theZone, "allBlue!", "*<cfxnone>")
cfxOwnedZones.hasAllBlue = nil
end
if cfxZones.hasProperty(theZone, "allRed!") then
cfxOwnedZones.allRed = cfxZones.getStringFromZoneProperty(theZone, "allRed!", "*<cfxnone>")
cfxOwnedZones.hasAllRed = nil
end
if cfxZones.hasProperty(theZone, "redOwned#") then
cfxOwnedZones.redOwned = cfxZones.getStringFromZoneProperty(theZone, "redOwned#", "*<cfxnone>")
end
if cfxZones.hasProperty(theZone, "blueOwned#") then
cfxOwnedZones.blueOwned = cfxZones.getStringFromZoneProperty(theZone, "blueOwned#", "*<cfxnone>")
end
if cfxZones.hasProperty(theZone, "neutralOwned#") then
cfxOwnedZones.neutralOwned = cfxZones.getStringFromZoneProperty(theZone, "neutralOwned#", "*<cfxnone>")
end
if cfxZones.hasProperty(theZone, "totalZones#") then
cfxOwnedZones.totalOwnedZones = cfxZones.getStringFromZoneProperty(theZone, "totalZones#", "*<cfxnone>")
end
-- numKeep, numCap, fastEval, easyContest -- numKeep, numCap, fastEval, easyContest
cfxOwnedZones.numCap = cfxZones.getNumberFromZoneProperty(theZone, "numCap", 1) -- minimal number of units required to cap zone cfxOwnedZones.numCap = cfxZones.getNumberFromZoneProperty(theZone, "numCap", 1) -- minimal number of units required to cap zone
cfxOwnedZones.numKeep = cfxZones.getNumberFromZoneProperty(theZone, "numKeep", 0) -- number required to keep zone cfxOwnedZones.numKeep = cfxZones.getNumberFromZoneProperty(theZone, "numKeep", 0) -- number required to keep zone
@ -490,6 +774,21 @@ function cfxOwnedZones.readConfigZone(theZone)
-- winSound, loseSound -- winSound, loseSound
cfxOwnedZones.winSound = cfxZones.getStringFromZoneProperty(theZone, "winSound", "Quest Snare 3.wav" ) cfxOwnedZones.winSound = cfxZones.getStringFromZoneProperty(theZone, "winSound", "Quest Snare 3.wav" )
cfxOwnedZones.loseSound = cfxZones.getStringFromZoneProperty(theZone, "loseSound", "Death BRASS.wav") cfxOwnedZones.loseSound = cfxZones.getStringFromZoneProperty(theZone, "loseSound", "Death BRASS.wav")
-- capture options
cfxOwnedZones.groundCap = cfxZones.getBoolFromZoneProperty(theZone, "groundCap", true)
cfxOwnedZones.navalCap = cfxZones.getBoolFromZoneProperty(theZone, "navalCap", false)
cfxOwnedZones.heloCap = cfxZones.getBoolFromZoneProperty(theZone, "heloCap")
cfxOwnedZones.fixWingCap = cfxZones.getBoolFromZoneProperty(theZone, "fixWingCap")
-- colors for line and fill
cfxOwnedZones.redLine = cfxZones.getRGBAVectorFromZoneProperty(theZone, "redLine", {1.0, 0, 0, 1.0})
cfxOwnedZones.redFill = cfxZones.getRGBAVectorFromZoneProperty(theZone, "redFill", {1.0, 0, 0, 0.2})
cfxOwnedZones.blueLine = cfxZones.getRGBAVectorFromZoneProperty(theZone, "blueLine", {0.0, 0, 1.0, 1.0})
cfxOwnedZones.blueFill = cfxZones.getRGBAVectorFromZoneProperty(theZone, "blueFill", {0.0, 0, 1.0, 0.2})
cfxOwnedZones.neutralLine = cfxZones.getRGBAVectorFromZoneProperty(theZone, "neutralLine", {0.8, 0.8, 0.8, 1.0})
cfxOwnedZones.neutralFill = cfxZones.getRGBAVectorFromZoneProperty(theZone, "neutralFill", {0.8, 0.8, 0.8, 0.2})
end end
function cfxOwnedZones.init() function cfxOwnedZones.init()
@ -535,5 +834,12 @@ if not cfxOwnedZones.init() then
cfxOwnedZones = nil cfxOwnedZones = nil
end end
--[[--
masterOwner input for zones, overrides all else when not neutral
dont count zones that cant be conquered for allBlue/allRed
define color with #FF008080
--]]--

View File

@ -1,5 +1,5 @@
cfxZones = {} cfxZones = {}
cfxZones.version = "3.0.9" cfxZones.version = "3.1.0"
-- cf/x zone management module -- cf/x zone management module
-- reads dcs zones and makes them accessible and mutable -- reads dcs zones and makes them accessible and mutable
@ -127,6 +127,8 @@ cfxZones.version = "3.0.9"
- 3.0.7 - getPoint() can also get land y when passing true as second param - 3.0.7 - getPoint() can also get land y when passing true as second param
- 3.0.8 - new cfxZones.pointInOneOfZones(thePoint, zoneArray, useOrig) - 3.0.8 - new cfxZones.pointInOneOfZones(thePoint, zoneArray, useOrig)
- 3.0.9 - new getFlareColorStringFromZoneProperty() - 3.0.9 - new getFlareColorStringFromZoneProperty()
- 3.1.0 - new getRGBVectorFromZoneProperty()
new getRGBAVectorFromZoneProperty()
--]]-- --]]--
cfxZones.verbose = false cfxZones.verbose = false
@ -2221,6 +2223,56 @@ function cfxZones.getVectorFromZoneProperty(theZone, theProperty, minDims, defau
return nVec return nVec
end end
function cfxZones.getRGBVectorFromZoneProperty(theZone, theProperty, defaultVal)
if not defaultVal then defaultVal = {1.0, 1.0, 1.0} end
if #defaultVal ~=3 then defaultVal = {1.0, 1.0, 1.0} end
local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, "")
local sVec = dcsCommon.splitString(s, ",")
local nVec = {}
for i = 1, 3 do
n = sVec[i]
if n then n = tonumber(n) end
if not n then n = defaultVal[i] end
if n > 1.0 then n = 1.0 end
if n < 0 then n = 0 end
nVec[i] = n
end
return nVec
end
function cfxZones.getRGBAVectorFromZoneProperty(theZone, theProperty, defaultVal)
if not defaultVal then defaultVal = {1.0, 1.0, 1.0, 1.0} end
if #defaultVal ~=4 then defaultVal = {1.0, 1.0, 1.0, 1.0} end
local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, "")
local sVec = dcsCommon.splitString(s, ",")
local nVec = {}
for i = 1, 4 do
n = sVec[i]
if n then n = tonumber(n) end
if not n then n = defaultVal[i] end
if n > 1.0 then n = 1.0 end
if n < 0 then n = 0 end
nVec[i] = n
end
return nVec
end
function cfxZones.getRGBFromZoneProperty(theZone, theProperty, default)
--if not default then default = {1.0, 1.0, 1.0} end -- white
local rawRGB = cfxZones.getVectorFromZoneProperty(theZone, theProperty, 3, 1.0)
local retVal = {}
for i = 1, 3 do
local cp = rawRGB[i]
if cp > 1.0 then cp = 1.0 end
if cp < 0 then cp = 0 end
retVal[i] = cp
end
return retVal
end
function cfxZones.getSmokeColorStringFromZoneProperty(theZone, theProperty, default) -- smoke as 'red', 'green', or 1..5 function cfxZones.getSmokeColorStringFromZoneProperty(theZone, theProperty, default) -- smoke as 'red', 'green', or 1..5
if not default then default = "red" end if not default then default = "red" end
local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, default) local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, default)

View File

@ -1,19 +1,22 @@
factoryZone = {} factoryZone = {}
factoryZone.version = "1.0.0" factoryZone.version = "2.0.0"
factoryZone.verbose = false factoryZone.verbose = false
factoryZone.name = "factoryZone" factoryZone.name = "factoryZone"
--[[-- VERSION HISTORY --[[-- VERSION HISTORY
1.0.0 - refactored production part from cfxOwnedZones 1.xpcall 2.0.0 - refactored production part from cfxOwnedZones 1.xpcall
- "production" and "defenders" simplification
- now optional specification for red/blue
- use maxRadius from zone for spawning to support quad zones
--]]-- --]]--
factoryZone.requiredLibs = { factoryZone.requiredLibs = {
"dcsCommon", -- common is of course needed for everything "dcsCommon",
-- pretty stupid to check for this since we "cfxZones",
-- need common to invoke the check, but anyway
"cfxZones", -- Zones, of course
"cfxCommander", -- to make troops do stuff "cfxCommander", -- to make troops do stuff
"cfxGroundTroops", -- all produced troops rely on this
"cfxOwnedZones",
} }
factoryZone.zones = {} -- my factory zones factoryZone.zones = {} -- my factory zones
@ -28,7 +31,7 @@ factoryZone.repairTime = 200 -- time until we raplace one lost unit, also repair
-- is regularly verified and cut to size -- is regularly verified and cut to size
factoryZone.spawnedAttackers = {} factoryZone.spawnedAttackers = {}
-- factoryZone is a module that managers production of units -- factoryZone is a module that manages production of units
-- inside zones and can switch production based on who owns the -- inside zones and can switch production based on who owns the
-- zone. Zone ownership can by dynamic (by using OwnedZones or -- zone. Zone ownership can by dynamic (by using OwnedZones or
-- using scripts to change the 'owner' flag -- using scripts to change the 'owner' flag
@ -48,10 +51,6 @@ factoryZone.spawnedAttackers = {}
-- attackDelta - polar coord: r from zone center where attackers are spawned -- attackDelta - polar coord: r from zone center where attackers are spawned
-- attackPhi - polar degrees where attackers are to be spawned -- attackPhi - polar degrees where attackers are to be spawned
-- paused - will not spawn. default is false -- paused - will not spawn. default is false
-- unbeatable - can't be conquered by other side. default is false
-- untargetable - will not be targeted by either side. make unbeatable
-- owned zones untargetable, or they'll become a troop magnet for
-- zoneAttackers
-- --
-- M I S C -- M I S C
@ -66,29 +65,39 @@ function factoryZone.getFactoryZoneByName(zName)
end end
function factoryZone.addFactoryZone(aZone) function factoryZone.addFactoryZone(aZone)
aZone.worksFor = cfxZones.getCoalitionFromZoneProperty(aZone, "factory", 0) -- currently unused, have RED/BLUE separate types --aZone.worksFor = cfxZones.getCoalitionFromZoneProperty(aZone, "factory", 0) -- currently unused, have RED/BLUE separate types
aZone.state = "init" aZone.state = "init"
aZone.timeStamp = timer.getTime() aZone.timeStamp = timer.getTime()
aZone.defendersRED = cfxZones.getStringFromZoneProperty(aZone, "defendersRED", "none")
aZone.defendersBLUE = cfxZones.getStringFromZoneProperty(aZone, "defendersBLUE", "none") -- set up production default
local factory = cfxZones.getStringFromZoneProperty(aZone, "factory", "none")
local production = cfxZones.getStringFromZoneProperty(aZone, "production", factory)
local defenders = cfxZones.getStringFromZoneProperty(aZone, "defenders", factory)
if cfxZones.hasProperty(aZone, "attackersRED") then if cfxZones.hasProperty(aZone, "attackersRED") then
-- legacy support -- legacy support
aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "attackersRED", "none") aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "attackersRED", production)
else else
aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "productionRED", "none") aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "productionRED", production)
end end
if cfxZones.hasProperty(aZone, "attackersBLUE") then if cfxZones.hasProperty(aZone, "attackersBLUE") then
-- legacy support -- legacy support
aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "attackersBLUE", "none") aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "attackersBLUE", production)
else else
aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "productionBLUE", "none") aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "productionBLUE", production)
end end
-- set up defenders default, or use production / factory
aZone.defendersRED = cfxZones.getStringFromZoneProperty(aZone, "defendersRED", defenders)
aZone.defendersBLUE = cfxZones.getStringFromZoneProperty(aZone, "defendersBLUE", defenders)
aZone.formation = cfxZones.getStringFromZoneProperty(aZone, "formation", "circle_out") aZone.formation = cfxZones.getStringFromZoneProperty(aZone, "formation", "circle_out")
aZone.attackFormation = cfxZones.getStringFromZoneProperty(aZone, "attackFormation", "circle_out") -- cfxZones.getZoneProperty(aZone, "attackFormation") aZone.attackFormation = cfxZones.getStringFromZoneProperty(aZone, "attackFormation", "circle_out") -- cfxZones.getZoneProperty(aZone, "attackFormation")
aZone.spawnRadius = cfxZones.getNumberFromZoneProperty(aZone, "spawnRadius", aZone.radius-5) -- "-5" so they remaininside radius aZone.spawnRadius = cfxZones.getNumberFromZoneProperty(aZone, "spawnRadius", aZone.maxRadius-5) -- "-5" so they remaininside radius
aZone.attackRadius = cfxZones.getNumberFromZoneProperty(aZone, "attackRadius", aZone.radius) aZone.attackRadius = cfxZones.getNumberFromZoneProperty(aZone, "attackRadius", aZone.maxRadius)
aZone.attackDelta = cfxZones.getNumberFromZoneProperty(aZone, "attackDelta", 10) -- aZone.radius) aZone.attackDelta = cfxZones.getNumberFromZoneProperty(aZone, "attackDelta", 10) -- aZone.radius)
aZone.attackPhi = cfxZones.getNumberFromZoneProperty(aZone, "attackPhi", 0) aZone.attackPhi = cfxZones.getNumberFromZoneProperty(aZone, "attackPhi", 0)
@ -111,8 +120,6 @@ function factoryZone.addFactoryZone(aZone)
aZone.factoryTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "factoryTriggerMethod", "change") aZone.factoryTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "factoryTriggerMethod", "change")
end end
aZone.untargetable = cfxZones.getBoolFromZoneProperty(aZone, "untargetable", false)
factoryZone.zones[aZone.name] = aZone factoryZone.zones[aZone.name] = aZone
factoryZone.verifyZone(aZone) factoryZone.verifyZone(aZone)
end end
@ -125,154 +132,6 @@ function factoryZone.verifyZone(aZone)
end end
function factoryZone.getEnemyZonesFor(aCoalition)
-- when cfxOwnedZones is present, or it will return only those
-- else it scans all zones from cfxZones
local enemyZones = {}
local allZones = cfxZones.zones
if cfxOwnedZones then
allZones = cfxOwnedZones.zones
end
local ourEnemy = dcsCommon.getEnemyCoalitionFor(aCoalition)
for zKey, aZone in pairs(allZones) do
if aZone.owner == ourEnemy then -- only check enemy owned zones
-- note: will include untargetable zones
table.insert(enemyZones, aZone)
end
end
return enemyZones
end
function factoryZone.getNearestOwnedZoneToPoint(aPoint)
local shortestDist = math.huge
-- when cfxOwnedZones is present, or it will return only those
-- else it scans all zones from cfxZones
local closestZone = nil
local allZones = cfxZones.zones
if cfxOwnedZones then
allZones = cfxOwnedZones.zones
end
for zKey, aZone in pairs(allZones) do
local zPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and
currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function factoryZone.getNearestOwnedZone(theZone)
local shortestDist = math.huge
-- when cfxOwnedZones is present, or it will return only those
-- else it scans all zones from cfxZones
local closestZone = nil
local aPoint = cfxZones.getPoint(theZone)
local allZones = cfxZones.zones
if cfxOwnedZones then
allZones = cfxOwnedZones.zones
end
for zKey, aZone in pairs(allZones) do
local zPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function factoryZone.getNearestEnemyOwnedZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
-- when cfxOwnedZones is present, or it will return only those
-- else it scans all zones from cfxZones
local allZones = cfxZones.zones
if cfxOwnedZones then
allZones = cfxOwnedZones.zones
end
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies
local zPoint = cfxZones.getPoint(theZone)
for zKey, aZone in pairs(allZones) do
if targetNeutral then
-- return all zones that do not belong to us
if aZone.owner ~= theZone.owner then
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(aPoint, zPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- return zones that are taken by the Enenmy
if aZone.owner == ourEnemy then -- only check own zones
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function factoryZone.getNearestFriendlyZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies nor friends, all zones would be legal.
local zPoint = cfxZones.getPoint(theZone)
-- when cfxOwnedZones is present, or it will return only those
-- else it scans all zones from cfxZones
local allZones = cfxZones.zones
if cfxOwnedZones then
allZones = cfxOwnedZones.zones
end
for zKey, aZone in pairs(allZones) do
if targetNeutral then
-- target all zones that do not belong to the enemy
if aZone.owner ~= ourEnemy then
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- only target zones that are taken by us
if aZone.owner == theZone.owner then -- only check own zones
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function factoryZone.enemiesRemaining(aZone)
if factoryZone.getNearestEnemyOwnedZone(aZone) then return true end
return false
end
function factoryZone.spawnAttackTroops(theTypes, aZone, aCoalition, aFormation) function factoryZone.spawnAttackTroops(theTypes, aZone, aCoalition, aFormation)
local unitTypes = {} -- build type names local unitTypes = {} -- build type names
-- split theTypes into an array of types -- split theTypes into an array of types
@ -337,7 +196,7 @@ end
-- --
function factoryZone.sendOutAttackers(aZone) function factoryZone.sendOutAttackers(aZone)
-- sanity check: never done for non-neutral zones -- sanity check: never done for neutral zones
if aZone.owner == 0 then if aZone.owner == 0 then
if aZone.verbose or factoryZone.verbose then if aZone.verbose or factoryZone.verbose then
trigger.action.outText("+++factZ: SendAttackers invoked for NEUTRAL zone <" .. aZone.name .. ">", 30) trigger.action.outText("+++factZ: SendAttackers invoked for NEUTRAL zone <" .. aZone.name .. ">", 30)
@ -346,7 +205,7 @@ function factoryZone.sendOutAttackers(aZone)
end end
-- only spawn if there are zones to attack -- only spawn if there are zones to attack
if not factoryZone.enemiesRemaining(aZone) then if not cfxOwnedZones.enemiesRemaining(aZone) then
if factoryZone.verbose then if factoryZone.verbose then
trigger.action.outText("+++factZ - no enemies, resting ".. aZone.name, 30) trigger.action.outText("+++factZ - no enemies, resting ".. aZone.name, 30)
end end
@ -832,11 +691,13 @@ function factoryZone.readConfigZone(theZone)
factoryZone.attackingTime = cfxZones.getNumberFromZoneProperty(theZone, "attackingTime", 300) factoryZone.attackingTime = cfxZones.getNumberFromZoneProperty(theZone, "attackingTime", 300)
factoryZone.shockTime = cfxZones.getNumberFromZoneProperty(theZone, "shockTime", 200) factoryZone.shockTime = cfxZones.getNumberFromZoneProperty(theZone, "shockTime", 200)
factoryZone.repairTime = cfxZones.getNumberFromZoneProperty(theZone, "repairTime", 200) factoryZone.repairTime = cfxZones.getNumberFromZoneProperty(theZone, "repairTime", 200)
factoryZone.targetZones = "OWNED"
end end
function factoryZone.init() function factoryZone.init()
-- check libs -- check libs
if not dcsCommon.libCheck("cfx Owned Zones", if not dcsCommon.libCheck("cfx Factory Zones",
factoryZone.requiredLibs) then factoryZone.requiredLibs) then
return false return false
end end
@ -845,7 +706,7 @@ function factoryZone.init()
local theZone = cfxZones.getZoneByName("factoryZoneConfig") local theZone = cfxZones.getZoneByName("factoryZoneConfig")
factoryZone.readConfigZone(theZone) factoryZone.readConfigZone(theZone)
-- collect all owned zones by their 'factory' property -- collect all zones by their 'factory' property
-- start the process -- start the process
local pZones = cfxZones.zonesWithProperty("factory") local pZones = cfxZones.zonesWithProperty("factory")

View File

@ -1,5 +1,5 @@
limitedAirframes = {} limitedAirframes = {}
limitedAirframes.version = "1.5.3" limitedAirframes.version = "1.5.4"
limitedAirframes.verbose = false limitedAirframes.verbose = false
limitedAirframes.enabled = true -- can be turned off limitedAirframes.enabled = true -- can be turned off
limitedAirframes.userCanToggle = true -- F10 menu? limitedAirframes.userCanToggle = true -- F10 menu?
@ -56,6 +56,7 @@ limitedAirframes.requiredLibs = {
when autoCSAR is active when autoCSAR is active
- 1.5.3 - ... but do allow it if not coming from 'ejected' so ditching - 1.5.3 - ... but do allow it if not coming from 'ejected' so ditching
a plane will again create CSAR missions a plane will again create CSAR missions
1.5.4 - red# and blue# instead of #red and #blue
--]]-- --]]--
@ -147,8 +148,16 @@ function limitedAirframes.readConfigZone()
limitedAirframes.maxBlue = cfxZones.getNumberFromZoneProperty(theZone, "maxBlue", -1) limitedAirframes.maxBlue = cfxZones.getNumberFromZoneProperty(theZone, "maxBlue", -1)
limitedAirframes.numRed = cfxZones.getStringFromZoneProperty(theZone, "#red", "*none") if cfxZones.hasProperty(theZone, "#red") then
limitedAirframes.numBlue = cfxZones.getStringFromZoneProperty(theZone, "#blue", "*none") limitedAirframes.numRed = cfxZones.getStringFromZoneProperty(theZone, "#red", "*none")
else
limitedAirframes.numRed = cfxZones.getStringFromZoneProperty(theZone, "red#", "*none")
end
if cfxZones.hasProperty(theZone, "#blue") then
limitedAirframes.numBlue = cfxZones.getStringFromZoneProperty(theZone, "#blue", "*none")
else
limitedAirframes.numBlue = cfxZones.getStringFromZoneProperty(theZone, "blue#", "*none")
end
limitedAirframes.redWinsFlag = cfxZones.getStringFromZoneProperty(theZone, "redWins!", "*none") limitedAirframes.redWinsFlag = cfxZones.getStringFromZoneProperty(theZone, "redWins!", "*none")

52
modules/mxObjects.lua Normal file
View File

@ -0,0 +1,52 @@
mxObjects = {}
function mxObjects.getObjectFreePoly(layerName, polyName, rel) -- omit rel to get absolute points, else pass 'true' to get relative to first point.
if not rel then rel = false end -- relative or absolute
if not env.mission.drawings then
trigger.action.outText("+++mxO: Mission has no drawings.", 30)
return {}
end
local drawings = env.mission.drawings
local layers = drawings["layers"]
if not layers then
trigger.action.outText("+++mxO: Mission has no layers in drawing", 30)
return {}
end
local theLayer = nil
for idx, aLayer in pairs(layers) do
if aLayer.name == layerName then
theLayer = aLayer
end
end
if not theLayer then
trigger.action.outText("+++mxO: No layer named <" .. layerName .. "> in Mission", 30)
return {}
end
local objects = theLayer.objects
if not objects then
trigger.action.outText("+++mxO: No objects in layer <" .. layerName .. ">", 30)
return {}
end
-- scan objects for a "free" mode poly with name polyName
for idx, theObject in pairs(objects) do
if theObject.polygonMode == "free" and theObject.name == polyName then
local poly = {}
for idp, thePoint in pairs(theObject.points) do
local p = {}
p.x = thePoint.x
p.y = thePoint.y
if not rel then
p.x = p.x + theObject.mapX
p.y = p.y + theObject.mapY
end
poly[idp] = p
end
return poly
end
end
trigger.action.outText("+++mxO: no polygon named <" .. polyName .. "> in layer <" ..layerName .. ">", 30)
return {}
end

View File

@ -8,6 +8,7 @@ playerZone.playerZones = {}
--[[-- --[[--
Version History Version History
1.0.0 - Initial version 1.0.0 - Initial version
1.0.1 - pNum --> pNum#
--]]-- --]]--
@ -22,7 +23,9 @@ function playerZone.createPlayerZone(theZone)
theZone.pzMethod = cfxZones.getStringFromZoneProperty(theZone, "pwMethod", "inc") theZone.pzMethod = cfxZones.getStringFromZoneProperty(theZone, "pwMethod", "inc")
end end
if cfxZones.hasProperty(theZone, "pNum") then if cfxZones.hasProperty(theZone, "pNum#") then
theZone.pNum = cfxZones.getStringFromZoneProperty(theZone, "pNum#", "none")
elseif cfxZones.hasProperty(theZone, "pNum") then
theZone.pNum = cfxZones.getStringFromZoneProperty(theZone, "pNum", "none") theZone.pNum = cfxZones.getStringFromZoneProperty(theZone, "pNum", "none")
end end

View File

@ -0,0 +1,320 @@
stopGap = {}
stopGap.version = "1.0.4 STANDALONE"
stopGap.verbose = false
stopGap.ssbEnabled = true
stopGap.ignoreMe = "-sg"
--[[--
Written and (c) 2023 by Christian Franz
Replace all player units with static aircraft until the first time
that a player slots into that plane. Static is then replaced with live
player unit. For Multiplayer the small (server-only) script "StopGapGUI" is required
For aircraft/helo carriers, no player planes are replaced with statics
STRONGLY RECOMMENDED:
- Use single-unit player groups.
- Use 'start from ground hot/cold' to be able to control initial aircraft orientation
To selectively exempt player units from stopGap, add a '-sg' to their name
Version History
1.0.0 - Initial version
1.0.1 - update / replace statics after slots become free
1.0.3 - server plug-in logic for SSB, sgGUI
1.0.4 - player units or groups that end in '-sg' are not stop-gapped
--]]--
stopGap.standInGroups ={}
stopGap.myGroups = {} -- for fast look-up of mx orig data
--
-- one-time start-up processing
--
-- in DCS, a group with one or more players only allocates when
-- the first player in the group enters the game.
--
cfxMX = {} -- local copy of cfxMX mission data cross reference tool
cfxMX.playerGroupByName = {} -- returns data only if a player is in group
cfxMX.countryByName ={} -- county of group named
function cfxMX.createCrossReferences()
-- tip o' hat to Mist for scanning mission struct.
for coa_name_miz, coa_data in pairs(env.mission.coalition) do -- iterate all coalitions
local coa_name = coa_name_miz
if string.lower(coa_name_miz) == 'neutrals' then -- remove 's' at neutralS
coa_name = 'neutral'
end
-- directly convert coalition into number for easier access later
local coaNum = 0
if coa_name == "red" then coaNum = 1 end
if coa_name == "blue" then coaNum = 2 end
if type(coa_data) == 'table' then -- coalition = {bullseye, nav_points, name, county},
-- with county being an array
if coa_data.country then -- make sure there a country table for this coalition
for cntry_id, cntry_data in pairs(coa_data.country) do -- iterate all countries for this
-- per country = {id, name, vehicle, helicopter, plane, ship, static}
local countryName = string.lower(cntry_data.name)
local countryID = cntry_data.id
if type(cntry_data) == 'table' then -- filter strings .id and .name
for obj_type_name, obj_type_data in pairs(cntry_data) do
if obj_type_name == "helicopter" or
obj_type_name == "ship" or
obj_type_name == "plane" or
obj_type_name == "vehicle" or
obj_type_name == "static" -- what about "cargo"?
then -- (so it's not id or name)
local category = obj_type_name
if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's at least one group!
for group_num, group_data in pairs(obj_type_data.group) do
local aName = group_data.name
cfxMX.countryByName[aName] = countryID
-- now iterate all units in this group
-- for player info and ID
for unit_num, unit_data in pairs(group_data.units) do
if unit_data.skill then
if unit_data.skill == "Client" or unit_data.skill == "Player" then
cfxMX.playerGroupByName[aName] = group_data -- inefficient, but works
end -- if unit skill client
end -- if is player/client skill
end -- for all units
end -- for all groups
end --if has category data
end --if plane, helo etc... category
end --for all objects in country
end --if has country data
end --for all countries in coalition
end --if coalition has country table
end -- if there is coalition data
end --for all coalitions in mission
end
function stopGap.staticMXFromUnitMX(theGroup, theUnit)
-- enter with MX data blocks
-- build a static object from mx unit data
local theStatic = {}
theStatic.x = theUnit.x
theStatic.y = theUnit.y
theStatic.livery_id = theUnit.livery_id -- if exists
theStatic.heading = theUnit.heading -- may need some attention
theStatic.type = theUnit.type
theStatic.name = theUnit.name -- will magically be replaced with player unit
theStatic.cty = cfxMX.countryByName[theGroup.name]
return theStatic
end
function stopGap.isGroundStart(theGroup)
-- look at route
if not theGroup.route then return false end
local route = theGroup.route
local points = route.points
if not points then return false end
local ip = points[1]
if not ip then return false end
local action = ip.action
if action == "Fly Over Point" then return false end
if action == "Turning Point" then return false end
if action == "Landing" then return false end
-- looks like aircraft is on the ground
-- but is it in water (carrier)?
local u1 = theGroup.units[1]
local sType = land.getSurfaceType(u1) -- has fields x and y
if sType == 3 then return false end
if stopGap.verbose then
trigger.action.outText("Player Group <" .. theGroup.name .. "> GROUND BASED: " .. action .. " land type " .. sType, 30)
end
return true
end
function stopGap.createStandInsForMXGroup(group)
local allUnits = group.units
if group.name:sub(-#stopGap.ignoreMe) == stopGap.ignoreMe then
if stopGap.verbose then
trigger.action.outText("<<skipping group " .. group.name .. ">>", 30)
end
return nil
end
local theStaticGroup = {}
for idx, theUnit in pairs (allUnits) do
if (theUnit.skill == "Client" or theUnit.skill == "Player")
and (theUnit.name:sub(-#stopGap.ignoreMe) ~= stopGap.ignoreMe)
then
local theStaticMX = stopGap.staticMXFromUnitMX(group, theUnit)
local theStatic = coalition.addStaticObject(theStaticMX.cty, theStaticMX)
theStaticGroup[theUnit.name] = theStatic -- remember me
if stopGap.verbose then
trigger.action.outText("Stop-gap-ing <" .. theUnit.name .. ">", 30)
end
else
if stopGap.verbose then
trigger.action.outText("<<skipping unit " .. theUnit.name .. ">>", 30)
end
end
end
return theStaticGroup
end
function stopGap.initGaps()
-- when we enter, all slots are emptry
-- and we populate all slots
-- with their static representations
for name, group in pairs (cfxMX.playerGroupByName) do
-- check to see if this group is on the ground at parking
-- by looking at the first waypoint
if stopGap.isGroundStart(group) then
-- this is one of ours!
group.sgName = "SG"..group.name -- flag name for MP
trigger.action.setUserFlag(group.sgName, 0) -- mark unengaged
stopGap.myGroups[name] = group
-- see if this group exists in-game already
local existing = Group.getByName(name)
if existing and Group.isExist(existing) then
if stopGap.verbose then
trigger.action.outText("+++stopG: group <" .. name .. "> already slotted, skipping", 30)
end
else
-- replace all groups entirely with static objects
---local allUnits = group.units
local theStaticGroup = stopGap.createStandInsForMXGroup(group)
-- remember this static group by its real name
stopGap.standInGroups[group.name] = theStaticGroup
end
end -- if groundtstart
end
end
--
-- event handling
--
function stopGap.removeStaticGapGroupNamed(gName)
for name, theStatic in pairs(stopGap.standInGroups[gName]) do
StaticObject.destroy(theStatic)
end
stopGap.standInGroups[gName] = nil
end
function stopGap:onEvent(event)
if not event then return end
if not event.id then return end
if not event.initiator then return end
local theUnit = event.initiator
if event.id == 15 then
if (not theUnit.getPlayerName) or (not theUnit:getPlayerName()) then
return
end -- no player unit.
local uName = theUnit:getName()
local theGroup = theUnit:getGroup()
local gName = theGroup:getName()
if stopGap.myGroups[gName] then
-- in case there were more than one units in this group,
-- also clear out the others. better safe than sorry
if stopGap.standInGroups[gName] then
stopGap.removeStaticGapGroupNamed(gName)
end
end
-- erase stopGapGUI flag, no longer required, unit
-- is now slotted into
trigger.action.setUserFlag("SG"..gName, 0)
end
end
--
-- update
--
function stopGap.update()
-- check every 1 second
timer.scheduleFunction(stopGap.update, {}, timer.getTime() + 1)
-- check if slots can be refilled or need to be vacated (MP)
for name, theGroup in pairs(stopGap.myGroups) do
if not stopGap.standInGroups[name] then
-- if there is no stand-in group, that group was slotted
-- or removed for ssb
local busy = true
local pGroup = Group.getByName(name)
if pGroup then
if Group.isExist(pGroup) then
else
busy = false -- no longer exists
end
else
busy = false -- nil group
end
-- now conduct ssb checks if enabled
if stopGap.ssbEnabled then
local ssbState = trigger.misc.getUserFlag(name)
if ssbState > 0 then
busy = true -- keep busy
end
end
-- check if StopGapGUI wants a word
local sgState = trigger.misc.getUserFlag(theGroup.sgName)
if sgState < 0 then
busy = true
-- count up for auto-release after n seconds
trigger.action.setUserFlag(theGroup.sgName, sgState + 1)
end
if busy then
-- players active in this group
else
local theStaticGroup = stopGap.createStandInsForMXGroup(theGroup)
stopGap.standInGroups[name] = theStaticGroup
end
else
-- plane is currently static and visible
-- check if this needs to change
local removeMe = false
if stopGap.ssbEnabled then
local ssbState = trigger.misc.getUserFlag(name)
if ssbState > 0 then removeMe = true end
end
local sgState = trigger.misc.getUserFlag(theGroup.sgName)
if sgState < 0 then removeMe = true end
if removeMe then
stopGap.removeStaticGapGroupNamed(name) -- also nils entry
if stopGap.verbose then
trigger.action.outText("+++StopG: [server command] remove static group <" .. name .. "> for SSB/SG server", 30)
end
end
end
end
end
--
-- get going
--
function stopGap.start()
-- run a cross reference on all mission data for palyer info
cfxMX.createCrossReferences()
-- fill player slots with static objects
stopGap.initGaps()
-- connect event handler
world.addEventHandler(stopGap)
-- start update in 10 seconds
timer.scheduleFunction(stopGap.update, {}, timer.getTime() + 1)
-- say hi!
trigger.action.outText("stopGap v" .. stopGap.version .. " running", 30)
return true
end
if not stopGap.start() then
trigger.action.outText("+++ aborted stopGap v" .. stopGap.version .. " -- start failed", 30)
stopGap = nil
end

370
modules/stopGaps.lua Normal file
View File

@ -0,0 +1,370 @@
stopGap = {}
stopGap.version = "1.0.4"
stopGap.verbose = false
stopGap.ssbEnabled = true
stopGap.ignoreMe = "-sg"
stopGap.requiredLibs = {
"dcsCommon",
"cfxZones",
"cfxMX",
}
--[[--
Written and (c) 2023 by Christian Franz
Replace all player units with static aircraft until the first time
that a player slots into that plane. Static is then replaced with live player unit.
For aircraft/helo carriers, no player planes are replaced with statics
For multiplayer, StopGapGUI must run on the server (only server)
STRONGLY RECOMMENDED FOR MISSION DESIGNERS:
- Use single-unit player groups.
- Use 'start from ground hot/cold' to be able to control initial aircraft orientation
To selectively exempt player units from stopGap, add a '-sg' to their name. Alternatively, use stopGap zones
Version History
1.0.0 - Initial version
1.0.1 - update / replace statics after slots become free again
1.0.2 - DML integration
- SSB integration
- on?
- off?
- onStart
- stopGap Zones
1.0.3 - server plug-in logic
1.0.4 - player units or groups that end in '-sg' are not stop-gapped
--]]--
stopGap.standInGroups = {}
stopGap.myGroups = {} -- for fast look-up of mx orig data
stopGap.stopGapZones = {} -- DML only
--
-- one-time start-up processing
--
-- in DCS, a group with one or more players only allocates when
-- the first player in the group enters the game.
--
function stopGap.staticMXFromUnitMX(theGroup, theUnit)
-- enter with MX data blocks
-- build a static object from mx unit data
local theStatic = {}
theStatic.x = theUnit.x
theStatic.y = theUnit.y
theStatic.livery_id = theUnit.livery_id -- if exists
theStatic.heading = theUnit.heading -- may need some attention
theStatic.type = theUnit.type
theStatic.name = theUnit.name -- will magically be replaced with player unit
theStatic.cty = cfxMX.countryByName[theGroup.name]
return theStatic
end
function stopGap.isGroundStart(theGroup)
-- look at route
if not theGroup.route then return false end
local route = theGroup.route
local points = route.points
if not points then return false end
local ip = points[1]
if not ip then return false end
local action = ip.action
if action == "Fly Over Point" then return false end
if action == "Turning Point" then return false end
if action == "Landing" then return false end
-- looks like aircraft is on the ground
-- but is it in water (carrier)?
local u1 = theGroup.units[1]
local sType = land.getSurfaceType(u1) -- has fields x and y
if sType == 3 then return false end
if stopGap.verbose then
trigger.action.outText("StopG: Player Group <" .. theGroup.name .. "> GROUND BASED: " .. action .. ", land type " .. sType, 30)
end
return true
end
function stopGap.ignoreMXUnit(theUnit)
local p = {x=theUnit.x, y=0, z=theUnit.y}
for idx, theZone in pairs(stopGap.stopGapZones) do
if theZone.sgIgnore and cfxZones.pointInZone(p, theZone) then
return true
end
end
return false
end
function stopGap.createStandInsForMXGroup(group)
local allUnits = group.units
if group.name:sub(-#stopGap.ignoreMe) == stopGap.ignoreMe then
if stopGap.verbose then
trigger.action.outText("+++StopG: <<skipping group " .. group.name .. ">>", 30)
end
return nil
end
local theStaticGroup = {}
for idx, theUnit in pairs (allUnits) do
if (theUnit.skill == "Client" or theUnit.skill == "Player")
and (theUnit.name:sub(-#stopGap.ignoreMe) ~= stopGap.ignoreMe)
and (not stopGap.ignoreMXUnit(theUnit))
then
local theStaticMX = stopGap.staticMXFromUnitMX(group, theUnit)
local theStatic = coalition.addStaticObject(theStaticMX.cty, theStaticMX)
theStaticGroup[theUnit.name] = theStatic -- remember me
if stopGap.verbose then
trigger.action.outText("+++StopG: adding static for <" .. theUnit.name .. ">", 30)
end
else
if stopGap.verbose then
trigger.action.outText("+++StopG: <<skipping unit " .. theUnit.name .. ">>", 30)
end
end
end
return theStaticGroup
end
function stopGap.initGaps()
-- turn on. May turn on any time, even during game
-- when we enter, all slots should be emptry
-- and we populate all slots. If slot in use, don't populate
-- with their static representations
for name, group in pairs (cfxMX.playerGroupByName) do
-- check to see if this group is on the ground at parking
-- by looking at the first waypoint
if stopGap.isGroundStart(group) then
-- this is one of ours!
group.sgName = "SG"..group.name -- flag name for MP
trigger.action.setUserFlag(group.sgName, 0) -- mark unengaged
stopGap.myGroups[name] = group
-- see if this group exists in-game already
local existing = Group.getByName(name)
if existing and Group.isExist(existing) then
if stopGap.verbose then
trigger.action.outText("+++stopG: group <" .. name .. "> already slotted, skipping", 30)
end
else
-- replace all groups entirely with static objects
---local allUnits = group.units
local theStaticGroup = stopGap.createStandInsForMXGroup(group)
-- remember this static group by its real name
stopGap.standInGroups[group.name] = theStaticGroup
end
end -- if groundtstart
end
end
function stopGap.turnOff()
-- remove all stand-ins
for gName, standIn in pairs (stopGap.standInGroups) do
for name, theStatic in pairs(standIn) do
StaticObject.destroy(theStatic)
end
end
stopGap.standInGroups = {}
end
function stopGap.turnOn()
-- populate all empty (non-taken) slots with stand-ins
stopGap.initGaps()
end
--
-- event handling
--
function stopGap.removeStaticGapGroupNamed(gName)
for name, theStatic in pairs(stopGap.standInGroups[gName]) do
StaticObject.destroy(theStatic)
end
stopGap.standInGroups[gName] = nil
end
function stopGap:onEvent(event)
if not event then return end
if not event.id then return end
if not event.initiator then return end
local theUnit = event.initiator
if event.id == 15 then
if (not theUnit.getPlayerName) or (not theUnit:getPlayerName()) then
return
end -- no player unit.
local uName = theUnit:getName()
local theGroup = theUnit:getGroup()
local gName = theGroup:getName()
if stopGap.myGroups[gName] then
-- in case there were more than one units in this group,
-- also clear out the others. better safe than sorry
if stopGap.standInGroups[gName] then
stopGap.removeStaticGapGroupNamed(gName)
end
end
-- erase stopGapGUI flag, no longer required, unit
-- is now slotted into
trigger.action.setUserFlag("SG"..gName, 0)
end
end
--
-- update, includes MP client check code
--
function stopGap.update()
-- check every second.
timer.scheduleFunction(stopGap.update, {}, timer.getTime() + 1)
-- check if signal for on? or off?
if stopGap.turnOn and cfxZones.testZoneFlag(stopGap, stopGap.turnOnFlag, "change", "lastTurnOnFlag") then
if not stopGap.enabled then
stopGap.turnOn()
end
stopGap.enabled = true
end
if stopGap.turnOff and cfxZones.testZoneFlag(stopGap, stopGap.turnOffFlag, "change", "lastTurnOffFlag") then
if stopGap.enabled then
stopGap.turnOff()
end
stopGap.enabled = false
end
if not stopGap.enabled then return end
-- check if slots can be refilled or need to be vacated (MP)
for name, theGroup in pairs(stopGap.myGroups) do
if not stopGap.standInGroups[name] then
-- if there is no stand-in group, that group was slotted
-- or removed for ssb
local busy = true
local pGroup = Group.getByName(name)
if pGroup then
if Group.isExist(pGroup) then
else
busy = false -- no longer exists
end
else
busy = false -- nil group
end
-- now conduct ssb checks if enabled
if stopGap.ssbEnabled then
local ssbState = trigger.misc.getUserFlag(name)
if ssbState > 0 then
busy = true -- keep busy
end
end
-- check if StopGapGUI wants a word
local sgState = trigger.misc.getUserFlag(theGroup.sgName)
if sgState < 0 then
busy = true
-- count up for auto-release after n seconds
trigger.action.setUserFlag(theGroup.sgName, sgState + 1)
end
if busy then
-- players active in this group
else
local theStaticGroup = stopGap.createStandInsForMXGroup(theGroup)
stopGap.standInGroups[name] = theStaticGroup
end
else
-- plane is currently static and visible
-- check if this needs to change
local removeMe = false
if stopGap.ssbEnabled then
local ssbState = trigger.misc.getUserFlag(name)
if ssbState > 0 then removeMe = true end
end
local sgState = trigger.misc.getUserFlag(theGroup.sgName)
if sgState < 0 then removeMe = true end
if removeMe then
stopGap.removeStaticGapGroupNamed(name) -- also nils entry
if stopGap.verbose then
trigger.action.outText("+++StopG: [server command] remove static group <" .. name .. "> for SSB/SG server", 30)
end
end
end
end
end
--
-- read stopGapZone
--
function stopGap.createStopGapZone(theZone)
local sg = cfxZones.getBoolFromZoneProperty(theZone, "stopGap", true)
if sg then theZone.sgIgnore = false else theZone.sgIgnore = true end
end
--
-- Read Config Zone
--
stopGap.name = "stopGapConfig" -- cfxZones compatibility here
function stopGap.readConfigZone(theZone)
-- currently nothing to do
stopGap.verbose = theZone.verbose
stopGap.ssbEnabled = cfxZones.getBoolFromZoneProperty(theZone, "sbb", true)
stopGap.enabled = cfxZones.getBoolFromZoneProperty(theZone, "onStart", true)
if cfxZones.hasProperty(theZone, "on?") then
stopGap.turnOnFlag = cfxZones.getStringFromZoneProperty(theZone, "on?", "*<none>")
stopGap.lastTurnOnFlag = trigger.misc.getUserFlag(stopGap.turnOnFlag)
end
if cfxZones.hasProperty(theZone, "off?") then
stopGap.turnOffFlag = cfxZones.getStringFromZoneProperty(theZone, "off?", "*<none>")
stopGap.lastTurnOffFlag = trigger.misc.getUserFlag(stopGap.turnOffFlag)
end
if stopGap.verbose then
trigger.action.outText("+++StopG: config read, verbose = YES", 30)
if stopGap.enabled then
trigger.action.outText("+++StopG: enabled", 30)
else
trigger.action.outText("+++StopG: turned off", 30)
end
end
end
--
-- get going
--
function stopGap.start()
if not dcsCommon.libCheck("cfx StopGap",
stopGap.requiredLibs)
then return false end
local theZone = cfxZones.getZoneByName("stopGapConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("stopGapConfig")
end
stopGap.readConfigZone(theZone)
-- collect exclusion zones
local pZones = cfxZones.zonesWithProperty("stopGap")
for k, aZone in pairs(pZones) do
stopGap.createStopGapZone(aZone)
stopGap.stopGapZones[aZone.name] = aZone
end
-- fill player slots with static objects
if stopGap.enabled then
stopGap.initGaps()
end
-- connect event handler
world.addEventHandler(stopGap)
-- start update in 10 seconds
timer.scheduleFunction(stopGap.update, {}, timer.getTime() + 1)
-- say hi!
trigger.action.outText("stopGap v" .. stopGap.version .. " running", 30)
return true
end
if not stopGap.start() then
trigger.action.outText("+++ aborted stopGap v" .. stopGap.version .. " -- startup failed", 30)
stopGap = nil
end

View File

@ -0,0 +1,22 @@
stopGapGUI = {}
stopGapGUI.version = "1.0.0"
stopGapGUI.fVal = -300 -- 5 minutes max block
--
-- Server Plug-In for StopGap mission script, only required for server
-- Put into (main DCS save folder)/Scripts/Hooks/ and restart DCS
--
function stopGapGUI.onPlayerTryChangeSlot(playerID, side, slotID)
if not slotID then return end
if slotID == "" then return end
if not DCS.isServer() then return end
if not DCS.isMultiplayer() then return end
local gName = DCS.getUnitProperty(slotID, DCS.UNIT_GROUPNAME)
if not gName then return end
local sgName = "SG" .. gName
-- tell all clients to remove this group's statics if they are deployed
net.dostring_in("server", " trigger.action.setUserFlag(\""..sgName.."\", " .. stopGapGUI.fVal .. "); ")
net.send_chat("+++SG: readying group <" .. sgName .. "> for slotting")
end
DCS.setUserCallbacks(stopGapGUI)

Binary file not shown.

Binary file not shown.