diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index f389eed..53185fb 100644 Binary files a/Doc/DML Documentation.pdf and b/Doc/DML Documentation.pdf differ diff --git a/Doc/DML Quick Reference.pdf b/Doc/DML Quick Reference.pdf index e2c529e..b2db7d4 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/cfxGroundTroops.lua b/modules/cfxGroundTroops.lua index de487c5..4a501e7 100644 --- a/modules/cfxGroundTroops.lua +++ b/modules/cfxGroundTroops.lua @@ -1,5 +1,5 @@ cfxGroundTroops = {} -cfxGroundTroops.version = "1.7.7" +cfxGroundTroops.version = "1.7.8" cfxGroundTroops.ups = 1 cfxGroundTroops.verbose = false cfxGroundTroops.requiredLibs = { @@ -67,7 +67,7 @@ cfxGroundTroops.deployedTroops = {} -- indexed by group name - makeTroopsEngageZone() sets 'moving' status to true - createGroundTroops() sets moving status to false - updateZoneAttackers() uses moving - + 1.7.8 - better guards before invoking ownedZones an entry into the deployed troop table has the following attributes - group - the group @@ -296,6 +296,10 @@ end function cfxGroundTroops.updateZoneAttackers(troop) 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 local newTargetZone = cfxGroundTroops.getClosestEnemyZone(troop) diff --git a/modules/cfxMX.lua b/modules/cfxMX.lua index 39ec941..1993566 100644 --- a/modules/cfxMX.lua +++ b/modules/cfxMX.lua @@ -32,7 +32,7 @@ cfxMX.unitIDbyName = {} cfxMX.groupDataByName = {} cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"... cfxMX.groupCoalitionByName = {} -cfxMX.countryByName ={} +cfxMX.countryByName ={} -- county of group named cfxMX.linkByName = {} cfxMX.allFixedByName = {} cfxMX.allHeloByName = {} diff --git a/modules/cfxOwnedZones.lua b/modules/cfxOwnedZones.lua index ca80266..f378a71 100644 --- a/modules/cfxOwnedZones.lua +++ b/modules/cfxOwnedZones.lua @@ -6,20 +6,33 @@ cfxOwnedZones.name = "cfxOwnedZones" --[[-- VERSION HISTORY 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 = { - "dcsCommon", -- common is of course needed for everything - -- pretty stupid to check for this since we - -- need common to invoke the check, but anyway - "cfxZones", -- Zones, of course + "dcsCommon", + "cfxZones", } cfxOwnedZones.zones = {} cfxOwnedZones.ups = 1 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 @@ -83,23 +96,28 @@ function cfxOwnedZones.drawZoneInMap(aZone) end if aZone.hidden then return end - local lineColor = {1.0, 0, 0, 1.0} -- red - local fillColor = {1.0, 0, 0, 0.2} -- red + local lineColor = aZone.redLine -- {1.0, 0, 0, 1.0} -- red + local fillColor = aZone.redFill -- {1.0, 0, 0, 0.2} -- red local owner = aZone.owner -- cfxOwnedZones.getOwnerForZone(aZone) if owner == 2 then - lineColor = {0.0, 0, 1.0, 1.0} - fillColor = {0.0, 0, 1.0, 0.2} + lineColor = aZone.blueLine -- {0.0, 0, 1.0, 1.0} + fillColor = aZone.blueFill -- {0.0, 0, 1.0, 0.2} elseif owner == 0 then - lineColor = {0.8, 0.8, 0.8, 1.0} - fillColor = {0.8, 0.8, 0.8, 0.2} + lineColor = aZone.neutralLine -- {0.8, 0.8, 0.8, 1.0} + fillColor = aZone.neutralFill -- {0.8, 0.8, 0.8, 0.2} end - local theShape = 2 -- circle +-- local theShape = 2 -- circle local markID = dcsCommon.numberUUID() - trigger.action.circleToAll(-1, markID, aZone.point, aZone.radius, lineColor, fillColor, 1, true, "") - aZone.markID = markID + if aZone.isCircle then + 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 function cfxOwnedZones.getOwnedZoneByName(zName) @@ -110,7 +128,7 @@ function cfxOwnedZones.getOwnedZoneByName(zName) end 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 aZone.conqueredFlag = cfxZones.getStringFromZoneProperty(aZone, "conquered!", "*") @@ -130,18 +148,25 @@ function cfxOwnedZones.addOwnedZone(aZone) if cfxZones.hasProperty(aZone, "neutral!") then aZone.neutralCap = cfxZones.getStringFromZoneProperty(aZone, "neutral!", "none") 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") 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.untargetable = cfxZones.getBoolFromZoneProperty(aZone, "untargetable", 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.drawZoneInMap(aZone) end @@ -246,24 +271,54 @@ function cfxOwnedZones.update() -- new. unit counting update cfxOwnedZones.updateSchedule = timer.scheduleFunction(cfxOwnedZones.update, {}, timer.getTime() + 1/cfxOwnedZones.ups) -- 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 theZone.numRed = 0 theZone.numBlue = 0 - -- count red units - local allRed = coalition.getGroups(1, Group.Category.GROUND) + -- count red units in zone for idx, aGroup in pairs(allRed) do if Group.isExist(aGroup) then if cfxOwnedZones.fastEval then -- we only check first unit that is alive 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() end else local allUnits = aGroup:getUnits() 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 end end @@ -271,19 +326,18 @@ function cfxOwnedZones.update() end end -- count blue units - local allBlue = coalition.getGroups(2, Group.Category.GROUND) for idx, aGroup in pairs(allBlue) do if Group.isExist(aGroup) then if cfxOwnedZones.fastEval then -- we only check first unit that is alive 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() end else local allUnits = aGroup:getUnits() 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 end end @@ -368,11 +422,51 @@ function cfxOwnedZones.update() -- update ownership flag if exists 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 -- 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 function cfxOwnedZones.sideOwnsAll(theSide) @@ -393,6 +487,162 @@ function cfxOwnedZones.hasOwnedZones() return false 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 @@ -415,8 +665,6 @@ function cfxOwnedZones.saveData() allZoneData[theZone.name] = zoneData 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 local flagInfo = {} flagInfo.neutral = cfxZones.getFlagValue(cfxOwnedZones.neutralTriggerFlag, cfxOwnedZones) @@ -478,10 +726,46 @@ function cfxOwnedZones.readConfigZone(theZone) cfxOwnedZones.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) cfxOwnedZones.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true) - cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r!", "*") - cfxOwnedZones.blueTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b!", "*") - cfxOwnedZones.neutralTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n!", "*") - + if cfxZones.hasProperty(theZone, "r!") then + cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r!", "*") + else + cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r#", "*") + end + if cfxZones.hasProperty(theZone, "b!") then + cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b!", "*") + else + cfxOwnedZones.blueTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b#", "*") + end + + if cfxZones.hasProperty(theZone, "n!") then + cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n!", "*") + else + cfxOwnedZones.neutralTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n#", "*") + end + + -- allXXX flags + if cfxZones.hasProperty(theZone, "allBlue!") then + cfxOwnedZones.allBlue = cfxZones.getStringFromZoneProperty(theZone, "allBlue!", "*") + cfxOwnedZones.hasAllBlue = nil + end + + if cfxZones.hasProperty(theZone, "allRed!") then + cfxOwnedZones.allRed = cfxZones.getStringFromZoneProperty(theZone, "allRed!", "*") + cfxOwnedZones.hasAllRed = nil + end + + if cfxZones.hasProperty(theZone, "redOwned#") then + cfxOwnedZones.redOwned = cfxZones.getStringFromZoneProperty(theZone, "redOwned#", "*") + end + if cfxZones.hasProperty(theZone, "blueOwned#") then + cfxOwnedZones.blueOwned = cfxZones.getStringFromZoneProperty(theZone, "blueOwned#", "*") + end + if cfxZones.hasProperty(theZone, "neutralOwned#") then + cfxOwnedZones.neutralOwned = cfxZones.getStringFromZoneProperty(theZone, "neutralOwned#", "*") + end + if cfxZones.hasProperty(theZone, "totalZones#") then + cfxOwnedZones.totalOwnedZones = cfxZones.getStringFromZoneProperty(theZone, "totalZones#", "*") + end -- numKeep, numCap, fastEval, easyContest 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 @@ -490,6 +774,21 @@ function cfxOwnedZones.readConfigZone(theZone) -- winSound, loseSound cfxOwnedZones.winSound = cfxZones.getStringFromZoneProperty(theZone, "winSound", "Quest Snare 3.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 function cfxOwnedZones.init() @@ -535,5 +834,12 @@ if not cfxOwnedZones.init() then cfxOwnedZones = nil 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 + +--]]-- diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index f7f89de..492d632 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "3.0.9" +cfxZones.version = "3.1.0" -- cf/x zone management module -- 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.8 - new cfxZones.pointInOneOfZones(thePoint, zoneArray, useOrig) - 3.0.9 - new getFlareColorStringFromZoneProperty() +- 3.1.0 - new getRGBVectorFromZoneProperty() + new getRGBAVectorFromZoneProperty() --]]-- cfxZones.verbose = false @@ -2221,6 +2223,56 @@ function cfxZones.getVectorFromZoneProperty(theZone, theProperty, minDims, defau return nVec 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 if not default then default = "red" end local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, default) diff --git a/modules/factoryZone.lua b/modules/factoryZone.lua index 36083c3..c6762e2 100644 --- a/modules/factoryZone.lua +++ b/modules/factoryZone.lua @@ -1,19 +1,22 @@ factoryZone = {} -factoryZone.version = "1.0.0" +factoryZone.version = "2.0.0" factoryZone.verbose = false factoryZone.name = "factoryZone" --[[-- 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 = { - "dcsCommon", -- common is of course needed for everything - -- pretty stupid to check for this since we - -- need common to invoke the check, but anyway - "cfxZones", -- Zones, of course + "dcsCommon", + "cfxZones", "cfxCommander", -- to make troops do stuff + "cfxGroundTroops", -- all produced troops rely on this + "cfxOwnedZones", } 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 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 -- zone. Zone ownership can by dynamic (by using OwnedZones or -- using scripts to change the 'owner' flag @@ -48,10 +51,6 @@ factoryZone.spawnedAttackers = {} -- attackDelta - polar coord: r from zone center where attackers are spawned -- attackPhi - polar degrees where attackers are to be spawned -- 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 @@ -66,29 +65,39 @@ function factoryZone.getFactoryZoneByName(zName) end 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.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 -- legacy support - aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "attackersRED", "none") + aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "attackersRED", production) else - aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "productionRED", "none") + aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "productionRED", production) end if cfxZones.hasProperty(aZone, "attackersBLUE") then -- legacy support - aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "attackersBLUE", "none") + aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "attackersBLUE", production) else - aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "productionBLUE", "none") + aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "productionBLUE", production) 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.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.attackRadius = cfxZones.getNumberFromZoneProperty(aZone, "attackRadius", aZone.radius) + aZone.spawnRadius = cfxZones.getNumberFromZoneProperty(aZone, "spawnRadius", aZone.maxRadius-5) -- "-5" so they remaininside radius + aZone.attackRadius = cfxZones.getNumberFromZoneProperty(aZone, "attackRadius", aZone.maxRadius) aZone.attackDelta = cfxZones.getNumberFromZoneProperty(aZone, "attackDelta", 10) -- aZone.radius) aZone.attackPhi = cfxZones.getNumberFromZoneProperty(aZone, "attackPhi", 0) @@ -111,8 +120,6 @@ function factoryZone.addFactoryZone(aZone) aZone.factoryTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "factoryTriggerMethod", "change") end - aZone.untargetable = cfxZones.getBoolFromZoneProperty(aZone, "untargetable", false) - factoryZone.zones[aZone.name] = aZone factoryZone.verifyZone(aZone) end @@ -125,154 +132,6 @@ function factoryZone.verifyZone(aZone) 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) local unitTypes = {} -- build type names -- split theTypes into an array of types @@ -337,7 +196,7 @@ end -- 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.verbose or factoryZone.verbose then trigger.action.outText("+++factZ: SendAttackers invoked for NEUTRAL zone <" .. aZone.name .. ">", 30) @@ -346,7 +205,7 @@ function factoryZone.sendOutAttackers(aZone) end -- only spawn if there are zones to attack - if not factoryZone.enemiesRemaining(aZone) then + if not cfxOwnedZones.enemiesRemaining(aZone) then if factoryZone.verbose then trigger.action.outText("+++factZ - no enemies, resting ".. aZone.name, 30) end @@ -832,11 +691,13 @@ function factoryZone.readConfigZone(theZone) factoryZone.attackingTime = cfxZones.getNumberFromZoneProperty(theZone, "attackingTime", 300) factoryZone.shockTime = cfxZones.getNumberFromZoneProperty(theZone, "shockTime", 200) factoryZone.repairTime = cfxZones.getNumberFromZoneProperty(theZone, "repairTime", 200) + factoryZone.targetZones = "OWNED" + end function factoryZone.init() -- check libs - if not dcsCommon.libCheck("cfx Owned Zones", + if not dcsCommon.libCheck("cfx Factory Zones", factoryZone.requiredLibs) then return false end @@ -845,7 +706,7 @@ function factoryZone.init() local theZone = cfxZones.getZoneByName("factoryZoneConfig") factoryZone.readConfigZone(theZone) - -- collect all owned zones by their 'factory' property + -- collect all zones by their 'factory' property -- start the process local pZones = cfxZones.zonesWithProperty("factory") diff --git a/modules/limitedAirframes.lua b/modules/limitedAirframes.lua index 6d7315e..46aa202 100644 --- a/modules/limitedAirframes.lua +++ b/modules/limitedAirframes.lua @@ -1,5 +1,5 @@ limitedAirframes = {} -limitedAirframes.version = "1.5.3" +limitedAirframes.version = "1.5.4" limitedAirframes.verbose = false limitedAirframes.enabled = true -- can be turned off limitedAirframes.userCanToggle = true -- F10 menu? @@ -56,6 +56,7 @@ limitedAirframes.requiredLibs = { when autoCSAR is active - 1.5.3 - ... but do allow it if not coming from 'ejected' so ditching 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.numRed = cfxZones.getStringFromZoneProperty(theZone, "#red", "*none") - limitedAirframes.numBlue = cfxZones.getStringFromZoneProperty(theZone, "#blue", "*none") + if cfxZones.hasProperty(theZone, "#red") then + 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") diff --git a/modules/mxObjects.lua b/modules/mxObjects.lua new file mode 100644 index 0000000..cb20244 --- /dev/null +++ b/modules/mxObjects.lua @@ -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 diff --git a/modules/playerZone.lua b/modules/playerZone.lua index 98703e1..bb9b825 100644 --- a/modules/playerZone.lua +++ b/modules/playerZone.lua @@ -8,6 +8,7 @@ playerZone.playerZones = {} --[[-- Version History 1.0.0 - Initial version + 1.0.1 - pNum --> pNum# --]]-- @@ -22,8 +23,10 @@ function playerZone.createPlayerZone(theZone) theZone.pzMethod = cfxZones.getStringFromZoneProperty(theZone, "pwMethod", "inc") end - if cfxZones.hasProperty(theZone, "pNum") then - theZone.pNum = cfxZones.getStringFromZoneProperty(theZone, "pNum", "none") + 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") end if cfxZones.hasProperty(theZone, "added!") then diff --git a/modules/stopGaps standalone.lua b/modules/stopGaps standalone.lua new file mode 100644 index 0000000..277690d --- /dev/null +++ b/modules/stopGaps standalone.lua @@ -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("<>", 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("<>", 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 + + diff --git a/modules/stopGaps.lua b/modules/stopGaps.lua new file mode 100644 index 0000000..7ed83e7 --- /dev/null +++ b/modules/stopGaps.lua @@ -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: <>", 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: <>", 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?", "*") + stopGap.lastTurnOnFlag = trigger.misc.getUserFlag(stopGap.turnOnFlag) + end + if cfxZones.hasProperty(theZone, "off?") then + stopGap.turnOffFlag = cfxZones.getStringFromZoneProperty(theZone, "off?", "*") + 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 + diff --git a/server modules/stopGapGUI.lua b/server modules/stopGapGUI.lua new file mode 100644 index 0000000..17262f5 --- /dev/null +++ b/server modules/stopGapGUI.lua @@ -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) \ No newline at end of file diff --git a/tutorial & demo missions/demo - My First Factory.miz b/tutorial & demo missions/demo - My First Factory.miz new file mode 100644 index 0000000..424f870 Binary files /dev/null and b/tutorial & demo missions/demo - My First Factory.miz differ diff --git a/tutorial & demo missions/demo - No gap, no glory.miz b/tutorial & demo missions/demo - No gap, no glory.miz new file mode 100644 index 0000000..499f81b Binary files /dev/null and b/tutorial & demo missions/demo - No gap, no glory.miz differ diff --git a/tutorial & demo missions/demo - Owned Zones and Factories.miz b/tutorial & demo missions/demo - Owned Zones and Factories.miz new file mode 100644 index 0000000..2d5f352 Binary files /dev/null and b/tutorial & demo missions/demo - Owned Zones and Factories.miz differ diff --git a/tutorial & demo missions/demo - once, twice, three times a maybe.miz b/tutorial & demo missions/demo - once, twice, three times a maybe.miz index e30f7c3..4dd5c14 100644 Binary files a/tutorial & demo missions/demo - once, twice, three times a maybe.miz and b/tutorial & demo missions/demo - once, twice, three times a maybe.miz differ