diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 4918a4e..f4a26d2 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 f62eeb3..32b40ae 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/FARPZones.lua b/modules/FARPZones.lua index e9d3436..99f4f08 100644 --- a/modules/FARPZones.lua +++ b/modules/FARPZones.lua @@ -1,5 +1,5 @@ FARPZones = {} -FARPZones.version = "2.0.0" +FARPZones.version = "2.1.0" FARPZones.verbose = false --[[-- Version History @@ -16,6 +16,15 @@ FARPZones.verbose = false 1.2.1 - now gracefully handles a FARP Zone that does not contain a FARP, but is placed beside it 2.0.0 - dmlZones + 2.0.1 - locking up FARPS for the first five seconds + loadMission handles lockup + FARPs now can show their name + showTitle attribute + refresh attribute (config) + 2.0.2 - clean-up + verbosity enhancements + 2.1.0 - integration with camp: needs repairs, produceResourceVehicles() + --]]-- @@ -23,6 +32,7 @@ FARPZones.requiredLibs = { "dcsCommon", "cfxZones", -- Zones, of course } +FARPZones.lockup = {} -- for the first 5 seconds of the game, released later -- *** DOES NOT EXTEND ZONES, USES OWN STRUCT *** -- *** SETS ZONE.OWNER IF PRESENT, POSSIBLE CONFLICT @@ -77,11 +87,11 @@ FARPZones.resourceTypes = { FARPZones.spinUpDelay = 30 -- seconds until FARP becomes operational after capture -FARPZones.allFARPZones = {} +FARPZones.allFARPZones = {} -- indexed by zone, returns dmlFARP struct FARPZones.startingUp = false -- not needed / read anywhere -- FARP ZONE ACCESS -function FARPZones.addFARPZone(aFARP) +function FARPZones.addFARPZone(aFARP) -- entry with dmlFARP struct FARPZones.allFARPZones[aFARP.zone] = aFARP end @@ -89,8 +99,8 @@ function FARPZones.removeFARPZone(aFARP) FARPZones.allFARPZones[aFARP.zone] = nil end -function FARPZones.getFARPForZone(aZone) - return FARPZones.allFARPZones[aZone] +function FARPZones.getFARPForZone(aZone) -- enter with zone + return FARPZones.allFARPZones[aZone] -- returns dmlFARP end function FARPZones.getFARPZoneByName(aName) @@ -153,7 +163,12 @@ function FARPZones.createFARPFromZone(aZone) theFarp.point = theFarp.mainFarp:getPoint() -- this is FARP, not zone!!! theFarp.owner = theFarp.mainFarp:getCoalition() aZone.owner = theFarp.owner --- end + -- lock up the faction until release in a few seconds after mission start + theFarp.mainFarp:autoCapture(false) + table.insert(FARPZones.lockup, theFarp.mainFarp) -- will be released in 5 seconds + if aZone.verbose then + trigger.action.outText("FARPzone <" .. aZone.name .. "> currently has owner <" .. aZone.owner .. ">", 30) + end -- get r and phi for defenders local rPhi = aZone:getVectorFromZoneProperty("rPhiHDef",3) @@ -191,7 +206,7 @@ function FARPZones.createFARPFromZone(aZone) theFarp.hideBlue = aZone:getBoolFromZoneProperty("hideBlue", false) theFarp.hideGrey = aZone:getBoolFromZoneProperty("hideGrey", false) theFarp.hidden = aZone:getBoolFromZoneProperty("hidden", false) - + theFarp.showTitle = aZone:getBoolFromZoneProperty("showTitle", true) theFarp.neutralProduction = aZone:getBoolFromZoneProperty("neutralProduction", false) return theFarp end @@ -206,6 +221,12 @@ function FARPZones.drawFARPCircleInMap(theFarp) theFarp.zone.markID = nil end + if theFarp.zone and theFarp.zone.titleID then + -- remove previous mark + trigger.action.removeMark(theFarp.zone.titleID) + theFarp.zone.titleID = nil + end + if theFarp.hideRed and theFarp.owner == 1 then -- hide only when red @@ -251,39 +272,12 @@ function FARPZones.drawFARPCircleInMap(theFarp) trigger.action.circleToAll(-1, markID, thePoint, 2000, lineColor, fillColor, 1, true, "") aZone.markID = markID - -end ---[[-- -function FARPZones.drawZoneInMap(aZone, owner) - -- owner is 0 = neutral, 1 = red, 2 = blue - -- will save markID in zone's markID - -- should be moved to cfxZones - -- should be able to only show owned - - if aZone.markID then - trigger.action.removeMark(aZone.markID) - end - - - local lineColor = {1.0, 0, 0, 1.0} -- red - local fillColor = {1.0, 0, 0, 0.2} -- red - - if owner == 2 then - lineColor = {0.0, 0, 1.0, 1.0} - fillColor = {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} + if theFarp.showTitle then + aZone.titleID = aZone:drawText(aZone.name, 16, lineColor, {0.8, 0.8, 0.8, 0.0}) end - local theShape = 2 -- circle - local markID = dcsCommon.numberUUID() - - trigger.action.circleToAll(-1, markID, aZone.point, aZone.radius, lineColor, fillColor, 1, true, "") - aZone.markID = markID - end ---]]-- + function FARPZones.scheduedProduction(args) -- args contain [aFarp, owner] @@ -306,6 +300,35 @@ function FARPZones.scheduedProduction(args) end end +function FARPZones.serviceNeedsRepair(theFarp) + if theFarp.owner == 0 then return false end + if not theFarp.resources then return false end -- no group yet + if not Group.isExist(theFarp.resources) then return true end + if theFarp.resources:getSize() < #FARPZones.resourceTypes then + return true + end + return false +end + +function FARPZones.produceResourceVehicles(theFarp, coa) + if theFarp.resources and Group.isExist(theFarp.resources) then --theFarp.resources:isExist() then + Group.destroy(theFarp.resources) --theFarp.resources:destroy() + theFarp.resources = nil + end + local unitTypes = FARPZones.resourceTypes -- an array + local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( + coa, + theFarp.name .. "-R" .. theFarp.count, -- must be unique + theFarp.resZone, + unitTypes, + "line_v", + theFarp.resHeading) + theFarp.resources = theGroup + theFarp.resourceData = theData + -- update unique counter + theFarp.count = theFarp.count + 1 +end + function FARPZones.produceVehicles(theFarp) local theZone = theFarp.zone -- trigger.action.outText("entering veh prod run for farp zone <" .. theZone.name .. ">, owner is <" .. theFarp.owner .. ">", 30) @@ -357,6 +380,9 @@ function FARPZones.produceVehicles(theFarp) theFarp.defenderData = theData end + -- spawn resource vehicles + FARPZones.produceResourceVehicles(theFarp, theCoalition) +--[[-- unitTypes = FARPZones.resourceTypes local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( theCoalition, @@ -366,7 +392,8 @@ function FARPZones.produceVehicles(theFarp) "line_v", theFarp.resHeading) theFarp.resources = theGroup - theFarp.resourceData = theData + theFarp.resourceData = theData +--]]-- -- update unique counter theFarp.count = theFarp.count + 1 end @@ -453,6 +480,20 @@ function FARPZones.somethingHappened(event) end +-- +-- Update / Refresh +-- + +function FARPZones.refreshMap() + timer.scheduleFunction(FARPZones.refreshMap, {}, timer.getTime() + FARPZones.refresh) + if FARPZones.verbose then + trigger.action.outText("+++Farp map refresh started", 30) + end + + for idx, theFARP in pairs(FARPZones.allFARPZones) do + FARPZones.drawFARPCircleInMap(theFARP) + end +end -- -- LOAD / SAVE @@ -503,6 +544,8 @@ function FARPZones.loadMission() if theFARP then theFARP.owner = fData.owner theFARP.zone.owner = fData.owner + local theAB = theFARP.mainFarp + theAB:setCoalition(theFARP.owner) -- FARP is in lockup. theFARP.defenderData = dcsCommon.clone(fData.defenderData) local groupData = fData.defenderData if groupData and #groupData.units > 0 then @@ -532,6 +575,14 @@ end -- -- Start -- +function FARPZones.releaseFARPS() +-- trigger.action.outText("Releasing hold on FARPS", 30) + for idx, aFarp in pairs(FARPZones.lockup) do + aFarp:autoCapture(true) +-- trigger.action.outText("releasing farp <" .. aFarp:getName() .. ">", 30) + end +end + function FARPZones.readConfig() local theZone = cfxZones.getZoneByName("farpZonesConfig") if not theZone then @@ -542,6 +593,8 @@ function FARPZones.readConfig() FARPZones.spinUpDelay = theZone:getNumberFromZoneProperty( "spinUpDelay", 30) + FARPZones.refresh = theZone:getNumberFromZoneProperty("refresh", -1) + end @@ -598,6 +651,12 @@ function FARPZones.start() FARPZones.startingUp = false -- not needed / read anywhere + timer.scheduleFunction(FARPZones.releaseFARPS, {}, timer.getTime() + 5) + + if FARPZones.refresh > 0 then + timer.scheduleFunction(FARPZones.refreshMap, {}, timer.getTime() + FARPZones.refresh) + end + trigger.action.outText("cf/x FARP Zones v" .. FARPZones.version .. " started", 30) return true end diff --git a/modules/SSBClient.lua b/modules/SSBClient.lua index 6548e3b..9610667 100644 --- a/modules/SSBClient.lua +++ b/modules/SSBClient.lua @@ -1,5 +1,5 @@ cfxSSBClient = {} -cfxSSBClient.version = "3.0.1" +cfxSSBClient.version = "4.0.0" cfxSSBClient.verbose = false cfxSSBClient.singleUse = false -- set to true to block crashed planes -- NOTE: singleUse (true) requires SSB to disable immediate respawn after kick @@ -8,76 +8,16 @@ cfxSSBClient.reUseAfter = -1 -- seconds for re-use delay cfxSSBClient.requiredLibs = { "dcsCommon", -- always - "cfxGroups", -- for slot access + "cfxMX", --"cfxGroups", -- for slot access "cfxZones", -- Zones, of course } --[[-- Version History - 1.0.0 - initial version - 1.1.0 - detect airfield by action and location, not group name - 1.1.1 - performance tuning. only read player groups once - - and remove in-air-start groups from scan. this requires - - ssb (server) be not modified - 1.2.0 - API to close airfields: invoke openAirFieldNamed() - and closeAirfieldNamed() with name as string (exact match required) - to block an airfield for any player aircraft. - Works for FARPS as well - API to associate a player group with any airfied's status (nil for unbind): - cfxSSBClient.bindGroupToAirfield(group, airfieldName) - API shortcut to unbind groups: cfxSSBClient.unbindGroup(group) - verbose messages now identify better: "+++SSB:" - keepInAirGroups option - 2.0.0 - include single-use ability: crashed airplanes are blocked from further use - - single-use can be turned off - - getPlayerGroupForGroupNamed() - - split setSlotAccess to single accessor - and interator - - reUseAfter option for single-use - - dcsCommon, cfxZones import - 2.0.1 - stricter verbosity: moved more comments to verbose only - 2.0.2 - health check code (initial) - - added verbosity - 2.0.3 - getPlayerName nil-trap on cloned player planes guard - in onEvent - 2.1.0 - slotState - - persistence - 3.0.0 - closing an airfield will not kick players who are active - - much better verbosity - - open? - - close? - - also persists closed airfield list - 3.0.1 - ability to detect if an airfield doesn't exist (late activate) - - - -WHAT IT IS -SSB Client is a small script that forms the client-side counterpart to -Ciribob's simple slot block. It will block slots for all client airframes -that are on an airfield that does not belong to the faction that currently -owns the airfield. - -REQUIRES CIRIBOB's SIMPLE SLOT BLOCK (SSB) TO RUN ON THE SERVER - -If run without SSB, your planes will not be blocked. - -In order to work, a plane that should be blocked when the airfield or -FARP doesn't belong to the player's faction, the group's first unit -must be within 3000 meters of the airfield and on the ground. -Previous versions of this script relied on group names. No longer. - - -WARNING: -If you modified ssb's flag values, this script will not work - -YOU DO NOT NEED TO ACTIVATE SBB, THIS SCRIPT DOES SO AUTOMAGICALLY - - + 4.0.0 - dmlZones + - cfxMX instead of cfxGroups --]]-- --- below value for enabled MUST BE THE SAME AS THE VALUE OF THE SAME NAME --- IN SSB. DEFAULT IS ZERO, AND THIS WILL WORK - cfxSSBClient.enabledFlagValue = 0 -- DO NOT CHANGE, MUST MATCH SSB cfxSSBClient.disabledFlagValue = cfxSSBClient.enabledFlagValue + 100 -- DO NOT CHANGE cfxSSBClient.allowNeutralFields = false -- set to FALSE if players can't spawn on neutral airfields @@ -124,26 +64,26 @@ end -- read client zones -- function cfxSSBClient.createClientZone(theZone) - local thePoint = cfxZones.getPoint(theZone) + local thePoint = theZone:getPoint() local theAF = cfxSSBClient.getClosestAirbaseTo(thePoint) local afName = theAF:getName() if cfxSSBClient.verbose or theZone.verbose then trigger.action.outText("+++ssbc: zone <" .. theZone.name .. "> linked to AF/FARP <" .. afName .. ">", 30) end theZone.afName = afName - theZone.ssbTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "ssbTriggerMethod", "change") + theZone.ssbTriggerMethod = theZone:getStringFromZoneProperty( "ssbTriggerMethod", "change") - if cfxZones.hasProperty(theZone, "open?") then - theZone.ssbOpen = cfxZones.getStringFromZoneProperty(theZone, "open?", "none") + if theZone:hasProperty("open?") then + theZone.ssbOpen = theZone:getStringFromZoneProperty("open?", "none") theZone.lastSsbOpen = cfxZones.getFlagValue(theZone.ssbOpen, theZone) end - if cfxZones.hasProperty(theZone, "close?") then - theZone.ssbClose = cfxZones.getStringFromZoneProperty(theZone, "close?", "none") + if theZone:hasProperty("close?") then + theZone.ssbClose = theZone:getStringFromZoneProperty("close?", "none") theZone.lastSsbClose = cfxZones.getFlagValue(theZone.ssbClose, theZone) end - theZone.ssbOpenOnStart = cfxZones.getBoolFromZoneProperty(theZone, "openOnStart", true) + theZone.ssbOpenOnStart = theZone:getBoolFromZoneProperty( "openOnStart", true) if not theZone.ssbOpenOnStart then cfxSSBClient.closeAirfieldNamed(theZone.afName) end @@ -494,7 +434,7 @@ end -- pre-process static player data to minimize -- processor load on checks function cfxSSBClient.processPlayerData() - cfxSSBClient.playerGroups = cfxGroups.getPlayerGroup() + cfxSSBClient.playerGroups = cfxMX.getPlayerGroup() local pGroups = cfxSSBClient.playerGroups local filteredPlayers = {} for idx, theGroup in pairs(pGroups) do @@ -511,7 +451,7 @@ end -- add airfield information to each player group function cfxSSBClient.processGroupData() - local pGroups = cfxGroups.getPlayerGroup() -- we want the group.name attribute + local pGroups = cfxMX.getPlayerGroup() -- we want the group.name attribute for idx, theGroup in pairs(pGroups) do -- we always use the first player's plane as referenced local playerData = theGroup.playerUnits[1] diff --git a/modules/asw.lua b/modules/asw.lua index c4cc1ba..f6c37ad 100644 --- a/modules/asw.lua +++ b/modules/asw.lua @@ -16,6 +16,7 @@ asw.fixes = {} -- all subs that we have a fix on. indexed by sub name Version History 1.0.0 - initial version 1.0.1 - integration with playerScore + 1.0.2 - new useSmoke attribute --]]-- @@ -151,7 +152,9 @@ function asw.dropBuoyFrom(theUnit) local theBuoy = asw.createBuoyForUnit(theUnit) -- mark point - dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor) + if asw.useSmoke then + dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor) + end theBuoy.smokeTimer = now + 5 * 60 -- add buoy to my inventory @@ -202,7 +205,9 @@ function asw.dropBuoyFromZone(theZone) local theBuoy = asw.createBuoyForZone(theZone) -- mark point - dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor) + if asw.useSmoke then + dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor) + end theBuoy.smokeTimer = now + 5 * 60 -- add buoy to my inventory @@ -369,7 +374,9 @@ function asw.updateBuoy(theBuoy, allSubs) -- see if we need to resmoke if now > theBuoy.smokeTimer then --env.info(" resmoking buoy, continue") - dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor) + if asw.useSmoke then + dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor) + end theBuoy.smokeTimer = now + 5 * 60 --env.info(" resmoke done, continue") end @@ -1042,7 +1049,8 @@ function asw.readConfigZone() asw.smokeColor = cfxZones.getSmokeColorStringFromZoneProperty(theZone, "smokeColor", "red") asw.smokeColor = dcsCommon.smokeColor2Num(asw.smokeColor) - + asw.useSmoke = theZone:getBoolFromZoneProperty("useSmoke", true) + asw.killScore = cfxZones.getNumberFromZoneProperty(theZone, "killScore", 0) if cfxZones.hasProperty(theZone, "killFeat") then diff --git a/modules/autoCSAR.lua b/modules/autoCSAR.lua index 1eda9f5..95e1b88 100644 --- a/modules/autoCSAR.lua +++ b/modules/autoCSAR.lua @@ -1,5 +1,5 @@ autoCSAR = {} -autoCSAR.version = "2.0.1" +autoCSAR.version = "2.1.0" autoCSAR.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -16,6 +16,7 @@ autoCSAR.trackedEjects = {} -- we start tracking on eject 2.0.0 - OOP, code clean-up 2.0.1 - fix for coalition change when ejected player changes coas or is forced to neutral - GC + 2.1.0 - persistence support --]]-- function autoCSAR.removeGuy(args) @@ -195,6 +196,33 @@ function autoCSAR.GC() autoCSAR.pilotInfo = filtered end +-- +-- load/save +-- + +function autoCSAR.saveData() + local theData = {} + theData.counter = autoCSAR.counter + return theData, autoCSAR.sharedData +end + +function autoCSAR.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("autoCSAR", autoCSAR.sharedData) + if not theData then + if autoCSAR.verbose then + trigger.action.outText("+++autoCSAR: no save data received, skipping.", 30) + end + return + end + if theData.counter then + autoCSAR.counter = theData.counter + end +end + +-- +-- GO! +-- function autoCSAR.start() -- lib check if not dcsCommon.libCheck then @@ -211,6 +239,16 @@ function autoCSAR.start() -- connect event handler world.addEventHandler(autoCSAR) + -- do persistence + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = autoCSAR.saveData + persistence.registerModule("autoCSAR", callbacks) + -- now load my data + autoCSAR.loadData() + end + -- start GC timer.scheduleFunction(autoCSAR.GC, {}, timer.getTime() + 1) diff --git a/modules/bank.lua b/modules/bank.lua index 30342b4..59675f8 100644 --- a/modules/bank.lua +++ b/modules/bank.lua @@ -1,10 +1,10 @@ bank = {} -bank.version = "0.0.0" +bank.version = "1.0.0" bank.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } -bank.acts = {} +bank.acts = {} -- 'accounts' function bank.addFunds(act, amt) if not act then act = "!!NIL!!" end @@ -57,9 +57,9 @@ function bank.getBalance(act) return true, curVal end -function bank.openAccount(act, amount) +function bank.openAccount(act, amount, oride) if not amount then amount = 0 end - if bank.acts[act] then return false end -- account exists + if bank.acts[act] and not oride then return false end -- account exists bank.acts[act] = amount return true end @@ -79,9 +79,43 @@ function bank.readConfigZone() bank.acts["blue"] = bank.blue bank.acts["neutral"] = bank.neutral + if theZone:hasProperty("sharedData") then -- future-proof + bank.sharedData = theZone:getStringFromZoneProperty("sharedData", "cfxNameMissing") + end + bank.verbose = theZone.verbose end +-- +-- load / save (persistence) +-- +function bank.saveData() + local theData = {} + -- save current score list. simple clone + local acts = dcsCommon.clone(bank.acts) + theData.acts = acts + + return theData, bank.sharedData +end + + +function bank.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("bank", bank.sharedData) + if not theData then + if bank.verbose then + trigger.action.outText("+++bank: no save data received, skipping.", 30) + end + return + end + + local acts = theData.acts + bank.acts = acts +end + +-- +-- start +-- function bank.start() -- lib check if not dcsCommon.libCheck then @@ -94,6 +128,17 @@ function bank.start() -- read config bank.readConfigZone() + + -- load data if persisted + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = bank.saveData + persistence.registerModule("bank", callbacks) + -- now load my data + bank.loadData() + end + trigger.action.outText("bank v" .. bank.version .. " started.", 30) return true diff --git a/modules/bombRange.lua b/modules/bombRange.lua index 90be393..f7729d4 100644 --- a/modules/bombRange.lua +++ b/modules/bombRange.lua @@ -1,5 +1,5 @@ bombRange = {} -bombRange.version = "1.1.1" +bombRange.version = "1.1.2" bombRange.dh = 1 -- meters above ground level burst bombRange.requiredLibs = { @@ -20,6 +20,8 @@ VERSION HISTORY also sampling kill events 1.1.1 - fixed reading smoke color for zone minor clean-up +1.1.2 - corrected bug when no bomb range is detected + --]]-- bombRange.bombs = {} -- live tracking bombRange.collector = {} -- post-impact collections for 0.5 secs @@ -492,7 +494,7 @@ function bombRange.impacted(weapon, target, finalPass) -- see if inside a range if #bombRange.ranges < 1 then - trigger.action.outText("+++bRng: No Bomb Ranges detected!") + trigger.action.outText("+++bRng: No Bomb Ranges detected!", 30) return -- no need to update anything end local minDist = math.huge diff --git a/modules/camp.lua b/modules/camp.lua index 364a04d..f079059 100644 --- a/modules/camp.lua +++ b/modules/camp.lua @@ -1,15 +1,27 @@ camp = {} camp.ups = 1 -camp.version = "0.0.0" +camp.version = "1.0.2" camp.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course "cfxMX", "bank" } - +-- AUTOMATICALLY INTEGRATES WITH income MODULE IF PRESENT +-- REQUIRES CLONEZONES TO RUN (BUT NOT TO START) +--[[-- +VERSION HISTORY + 1.0.0 - initial version + 1.0.1 - changed "Ground Repairs / Upgrades" to "Funds / Repairs / Upgrades" + - provided income info for camp if it exists + - provide income total if exists + - actionSound + - output sound with communications + 1.0.2 - integration with FARPZones +--]]-- -- -- CURRENTLY REQUIRES SINGLE-UNIT PLAYER GROUPS +-- REQUIRES CLONEZONES MODULE TO BE RUNNING, BUT NOT TO BE LOADED ON START -- camp.camps = {} -- all camps on the map camp.roots = {} -- all player group comms roots @@ -18,16 +30,27 @@ function camp.addCamp(theZone) camp.camps[theZone.name] = theZone end -function camp.getMyCurrentCamp(theUnit) -- returns first hit plaayer is in +function camp.getMyCurrentCamp(theUnit) -- returns first hit player is in + local coa = theUnit:getCoalition() local p = theUnit:getPoint() for idx, theCamp in pairs(camp.camps) do - if theCamp:pointInZone(p) then + if theCamp.owner == coa and theCamp:pointInZone(p) then return theCamp end end return nil end +function camp.getCampsForCoa(coa) + local myCamps = {} + for idx, theCamp in pairs(camp.camps) do + if theCamp.owner == coa then + table.insert(myCamps, theCamp) + end + end + return myCamps +end + function camp.createCampWithZone(theZone) -- look for all cloners inside my zone if theZone.verbose or camp.verbose then @@ -47,7 +70,7 @@ function camp.createCampWithZone(theZone) table.insert(cloners, aZone) if not aZone:hasProperty("blueOnly") then table.insert(redCloners, aZone) - end + end if not aZone:hasProperty("redOnly") then table.insert(blueCloners, aZone) end @@ -70,6 +93,12 @@ function camp.createCampWithZone(theZone) theZone.upgradable = theZone:getBoolFromZoneProperty("upgrade", true) theZone.repairCost = theZone:getNumberFromZoneProperty("repairCost", 100) theZone.upgradeCost = theZone:getNumberFromZoneProperty("upgradeCost", 3 * theZone.repairCost) + if theZone:hasProperty("FARP") then + theZone.isAlsoFARP = true + if theZone.verbose or camp.verbose then + trigger.action.outText("+++camp: <" .. theZone.name .. "> has FARP attached", 30) + end + end end -- @@ -93,7 +122,7 @@ function camp.processPlayers() for idx, gData in pairs(cfxMX.playerGroupByName) do gID = gData.groupId gName = gData.name - local theRoot = missionCommands.addSubMenuForGroup(gID, "Ground Repairs / Upgrades") + local theRoot = missionCommands.addSubMenuForGroup(gID, "Funds / Repairs / Upgrades") camp.roots[gName] = theRoot local c00 = missionCommands.addCommandForGroup(gID, "Theatre Overview", theRoot, camp.redirectTFunds, {gName, gID, "tfunds"}) local c0 = missionCommands.addCommandForGroup(gID, "Local Funds & Status Overview", theRoot, camp.redirectFunds, {gName, gID, "funds"}) @@ -126,12 +155,16 @@ function camp.doTFunds(args) local hasBalance, amount = bank.getBalance(coa) if not hasBalance then return end local msg = "\nYour faction currently has §" .. amount .. " available for repairs/upgrades.\n" - + local income = 0 -- now iterate all camps that are on my side for idx, theZone in pairs(camp.camps) do if theZone.owner == coa then msg = msg .. "\n - <" .. theZone.name .. ">" - + if theZone.income then + msg = msg .. " Income: §" .. theZone.income + income = income + theZone.income + end + if theZone.repairable and theZone.upgradable then msg = msg .. " (§" .. theZone.repairCost .. "/§" .. theZone.upgradeCost .. ")" if camp.zoneNeedsRepairs(theZone, coa) then @@ -165,8 +198,12 @@ function camp.doTFunds(args) end end end + if income > 0 then + msg = msg .. "\n\nTotal Income: §" .. income + end msg = msg .. "\n" trigger.action.outTextForGroup(gID, msg, 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) end function camp.doFunds(args) @@ -183,11 +220,13 @@ function camp.doFunds(args) if not Unit.isExist(theUnit) or theUnit:getLife() < 1 or theUnit:inAir() or dcsCommon.getUnitSpeed(theUnit) > 1 then trigger.action.outTextForGroup(gID, msg, 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end local theZone = camp.getMyCurrentCamp(theUnit) if not theZone or (not theZone.repairable) or theZone.owner ~= theUnit:getCoalition() then trigger.action.outTextForGroup(gID, msg, 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end @@ -204,17 +243,30 @@ function camp.doFunds(args) msg = msg .. "\nZone <" .. theZone.name .. "> is fully upgraded.\n" end trigger.action.outTextForGroup(gID, msg, 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) end -- -- REPAIRS -- + function camp.zoneNeedsRepairs(theZone, coa) - -- return true if this zone needs repairs, i.e. it has cloners that have a damaged clone set + -- return true if this zone needs repairs, i.e. it has cloners that have a damaged clone set or FARP resource vehicles are incomplete + if theZone.isAlsoFARP and FARPZones then + local theFarp = FARPZones.getFARPForZone(theZone) + if FARPZones.serviceNeedsRepair(theFarp) then + if theZone.verbose or camp.verbose then + trigger.action.outText("camp: <" .. theZone.name .. "> has FARP service is dinged up...", 30) + end + return true + -- WARNING: RETURNS BOOLEAN, not a dmlZone! + end + end + local myCloners = theZone.cloners if not coa then - trigger.action.outText("+++camp: warning: no coa on zoneNeedsRepair for zone <" .. theZone.name .. ">", 30) + trigger.action.outText("+++camp: warning: no coa on zoneNeedsRepairs for zone <" .. theZone.name .. ">", 30) elseif coa == 1 then myCloners = theZone.redCloners elseif coa == 2 then @@ -251,19 +303,23 @@ function camp.doRepairs(args) if theUnit:getLife() < 1 then return end if theUnit:inAir() then trigger.action.outTextForGroup(gID, "\nPlease land inside a fortified zone to order repairs\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end if dcsCommon.getUnitSpeed(theUnit) > 1 then trigger.action.outTextForGroup(gID, "\nYou must come to a complete stop before being able to order repairs\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end local theZone = camp.getMyCurrentCamp(theUnit) if not theZone or not theZone.repairable then trigger.action.outTextForGroup(gID, "\nYou are not inside a zone that can be repaired.\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end if theZone.owner ~= theUnit:getCoalition() then trigger.action.outTextForGroup(gID, "\nYou currently do not own zone <" .. theZone.name .. ">. Capture it first.\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end @@ -274,6 +330,7 @@ function camp.doRepairs(args) msg = msg .. "\nZone <" .. theZone.name .. "> can be upgraded.\n" end trigger.action.outTextForGroup(gID, msg, 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end @@ -285,7 +342,9 @@ function camp.doRepairs(args) end if amount < theZone.repairCost then - trigger.action.outTextForGroup(gID, "\nYou curently cannot afford repairs here\n", 30) +-- trigger.action.outTextForGroup(gID, "\nYou curently cannot afford repairs here\n", 30) + trigger.action.outTextForGroup(gID, "\nYou curently cannot afford repairs here (§" .. theZone.repairCost .. " required, you have §" .. amount .. ")\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end @@ -296,13 +355,23 @@ function camp.doRepairs(args) -- cloneZones.spawnWithCloner(theCloner) bank.withdawFunds(coa, theZone.repairCost) local ignore, remain = bank.getBalance(coa) - trigger.action.outTextForCoalition(coa, "\nZone <" .. theZone.name .. "> was repaired by <" .. pName .. + trigger.action.outTextForCoalition(coa, "\nZone <" .. theZone.name .. "> was ordered repaired by <" .. pName .. "> for §" .. theZone.repairCost .. ".\nFaction has §" .. remain .. " remaining funds.\n", 30) + trigger.action.outSoundForCoalition(coa, camp.actionSound) end function camp.repairZone(theZone, coa) theCloner = camp.zoneNeedsRepairs(theZone, coa) if not theCloner then return end + if type(theCloner) == "boolean" then -- at least farp was dinged up + local theFarp = FARPZones.getFARPForZone(theZone) + FARPZones.produceResourceVehicles(theFarp, coa) + if theZone.verbose or camp.verbose then + trigger.action.outText("+++camp: repaired FARP in camp <" .. theZone.name .. ">", 30) + end + end + theCloner = camp.zoneNeedsRepairs(theZone, coa) -- do again to see if other repairs are needed. FARP repairs come free with first fix + if not theCloner then return end cloneZones.despawnAll(theCloner) cloneZones.spawnWithCloner(theCloner) end @@ -311,7 +380,7 @@ end -- function camp.zoneNeedsUpgrades(theZone, coa) - -- return true if this zone can be upgraded, i.e. it has cloners that have an empty clone set + -- returns first cloner in this zone that can be upgraded, i.e. it has cloners that have an empty clone set if not theZone.upgradable then return nil end local myCloners = theZone.cloners @@ -352,30 +421,36 @@ function camp.doUpgrades(args) if not pName then pName = "" end if theUnit:inAir() then trigger.action.outTextForGroup(gID, "\nPlease land inside a fortified zone to order upgrades.\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end if dcsCommon.getUnitSpeed(theUnit) > 1 then trigger.action.outTextForGroup(gID, "\nYou must come to a complete stop before being able to order upgrades\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end local theZone = camp.getMyCurrentCamp(theUnit) if not theZone or not theZone.upgradable then trigger.action.outTextForGroup(gID, "\nYou are not inside a zone that can be upgraded.\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end if theZone.owner ~= theUnit:getCoalition() then trigger.action.outTextForGroup(gID, "\nYou currently do not own zone <" .. theZone.name .. ">. Capture it first.\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end if camp.zoneNeedsRepairs(theZone, coa) then trigger.action.outTextForGroup(gID, "\nZone <" .. theZone.name .. "> requires repairs before it can be upgraded.\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end -- if we get here, we are inside a zone that can be upgraded. see if it needs upgrades and then get upgrade cost and see if we have enough fund to do it if not camp.zoneNeedsUpgrades(theZone, coa) then trigger.action.outTextForGroup(gID, "\nZone <" .. theZone.name .. "> has been fully upgraded.\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end @@ -387,7 +462,8 @@ function camp.doUpgrades(args) end if amount < theZone.upgradeCost then - trigger.action.outTextForGroup(gID, "\nYou curently cannot afford an upgrade here\n", 30) + trigger.action.outTextForGroup(gID, "\nYou curently cannot afford an upgrade here (§" .. theZone.upgradeCost .. " required, you have §" .. amount .. ")\n", 30) + trigger.action.outSoundForGroup(gID, camp.actionSound) return end @@ -398,8 +474,9 @@ function camp.doUpgrades(args) -- bill it to side bank.withdawFunds(coa, theZone.upgradeCost) local ignore, remain = bank.getBalance(coa) - trigger.action.outTextForCoalition(coa, "\nZone <" .. theZone.name .. "> was upgraded by <" .. pName .. + trigger.action.outTextForCoalition(coa, "\nZone <" .. theZone.name .. "> was ordered upgraded by <" .. pName .. "> for §" .. theZone.upgradeCost .. ".\nFaction has §" .. remain .. " remaining funds.\n", 30) + trigger.action.outSoundForCoalition(coa, camp.actionSound) end -- can be called externally @@ -408,6 +485,32 @@ function camp.upgradeZone(theZone, coa) if not theCloner then return end cloneZones.spawnWithCloner(theCloner) end + +-- +-- API +-- +function camp.campsThatNeedRepairs(coa) -- returns the zones that need repairs + local repairs = {} + for idx, theZone in pairs(camp.camps) do + if theZone.repairable and theZone.owner == coa and camp.zoneNeedsRepairs(theZone, coa) then + table.insert(repairs, theZone) + end + end + + return repairs +end + +function camp.campsThatNeedUpgrades(coa) -- returns the zones that can be upgraded + local repairs = {} + for idx, theZone in pairs(camp.camps) do + if theZone.upgradable and theZone.owner == coa and camp.zoneNeedsUpgrades(theZone, coa) then + table.insert(repairs, theZone) + end + end + + return repairs +end + -- -- Config & Go -- @@ -417,7 +520,7 @@ function camp.readConfigZone() if not theZone then theZone = cfxZones.createSimpleZone("campConfig") end - + camp.actionSound = theZone:getStringFromZoneProperty("actionSound", "Quest Snare 3.wav") camp.verbose = theZone.verbose end diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 2b3686f..a5bc1e2 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "4.3.1" +cfxZones.version = "4.3.2" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -50,6 +50,8 @@ cfxZones.version = "4.3.1" - randomDelayFromPositiveRange also allows 0 - 4.3.1 - new drawText() for zones - dmlZones:getClosestZone() bridge +- 4.3.2 - new getListFromZoneProperty() + --]]-- -- @@ -2097,7 +2099,7 @@ end function cfxZones.drawZone(theZone, lineColor, fillColor, markID) if not theZone then return 0 end if not lineColor then lineColor = {0.8, 0.8, 0.8, 1.0} end - if not fillColor then fillColor = {0.8, 0.8, 0.8, 0.2} end + if not fillColor then fillColor = {0.8, 0.8, 0.8, 0.0} end if not markID then markID = dcsCommon.numberUUID() end if theZone.isCircle then @@ -2118,7 +2120,7 @@ function cfxZones.drawText(theZone, theText, fSize, lineColor, fillColor) if not theZone then return end if not fSize then fSize = 12 end if not lineColor then lineColor = {0.8, 0.8, 0.8, 1.0} end - if not fillColor then fillColor = lineColor end + if not fillColor then fillColor = {0, 0, 0, 0} end local markID = dcsCommon.numberUUID() local p = theZone:getPoint() local offset = {x = p.x, y = 0, z = p.z} @@ -2322,6 +2324,24 @@ function dmlZone:getPositiveRangeFromZoneProperty(theProperty, default, defaultm return lo, up end +function cfxZones.getListFromZoneProperty(theZone, theProperty, defaultItem) -- comma delimited + if not defaultItem then defaultItem = "default" end + + local theString = theZone:getStringFromZoneProperty(theProperty, defaultItem) + if dcsCommon.containsString(theString, ",") then + local theArray = dcsCommon.splitString(theString, ',') + theArray = dcsCommon.trimArray(theArray) + return theArray + else + return {theString} + end + + return nil +end + +function dmlZone:getListFromZoneProperty(theProperty, defaultItem) + return cfxZones.getListFromZoneProperty(self, theProperty, defaultItem) +end function cfxZones.hasProperty(theZone, theProperty) if not theProperty then diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index ba75efe..27001c4 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,5 +1,5 @@ cloneZones = {} -cloneZones.version = "2.2.0" +cloneZones.version = "2.2.1" cloneZones.verbose = false cloneZones.requiredLibs = { "dcsCommon", -- always @@ -50,6 +50,8 @@ cloneZones.respawnOnGroupID = true - damaged! output - health# output - persistence: persist oSize and set lastSize + 2.2.1 - verbosity updates for post-check + - if cloned group is late activation, turn it off --]]-- -- @@ -1054,7 +1056,12 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) local dataToSpawn = {} -- temp save so we can connect in-group references for idx, aGroupName in pairs(theZone.cloneNames) do - local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(aGroupName) + local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(aGroupName) -- fetches a clone! + -- sanity checks: lateActivation etc + if rawData.lateActivation then + trigger.action.outText("+++clnZ: WARNING - clone group <" .. rawData.name .. "> in cloner <" .. theZone.name .. "> is set to 'late activation'. Ignored.", 30) + rawData.lateActivation = false + end rawData.CZorigName = rawData.name -- save original group name local origID = rawData.groupId -- save original group ID rawData.CZorigID = origID @@ -1287,7 +1294,9 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) end cloneZones.unitXlate[aUnit.CZorigID] = uID else - trigger.action.outText("clnZ: post-clone verifiaction failed for unit <" .. uName .. ">: not found", 30) + if spawnZone.verbose then + trigger.action.outText("clnZ: post-clone verifiaction failed for unit <" .. uName .. ">: not found", 30) + end end end diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index 2cebf1c..21a6c17 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -1,5 +1,5 @@ csarManager = {} -csarManager.version = "3.2.7" +csarManager.version = "3.4.0" csarManager.ups = 1 --[[-- VERSION HISTORY @@ -42,12 +42,15 @@ csarManager.ups = 1 - useRanks option 3.2.6 - inBuiltup analogon to cloner 3.2.7 - createCSARForParachutist now supports optional coa (autoCSAR) - + 3.3.0 - persistence support + 3.4.0 - global timeLimit option in config zone + - fixes expiration bug when persisting data INTEGRATES AUTOMATICALLY WITH playerScore INTEGRATES WITH LIMITED AIRFRAMES INTEGRATES AUTOMATICALLY WITH SCRIBE + SUPPORTS PERSISTENCE --]]-- -- modules that need to be loaded BEFORE I run @@ -201,10 +204,22 @@ function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, newMission.timeStamp = timer.getTime() -- now + -- if no time limit given but csarManager's own global dictates it, + -- set it now + if not timeLimit and csarManager.timeLimit then + if csarManager.verbose then + trigger.action.outText("+++csar: setting GLOBAL csar time limit (" .. csarManager.timeLimit[1] .. "," .. csarManager.timeLimit[2] .. ") for new mission " .. name, 30) + end + timeLimit = csarManager.timeLimit + end + -- set timeLimit if enabled if timeLimit then local theLimit = cfxZones.randomDelayFromPositiveRange(timeLimit[1], timeLimit[2]) * 60 newMission.expires = timer.getTime() + theLimit + if csarManager.verbose then + trigger.action.outText("+++csar: setting time limit to expire in (" .. math.floor(theLimit/60) .. ") minutes for mission " .. name, 30) + end end -- update counter and return @@ -794,10 +809,10 @@ function csarManager.doListCSARRequests(args) local status = "alive" if mission.expires then delta = math.floor ((mission.expires - now) / 60) - if delta < 10 then status = "+deteriorating+" end - if delta < 5 then status = "*critical*" end + if delta < 30 then status = "+deteriorating+" end + if delta < 15 then status = "*critical*" end if csarManager.verbose then - status = status .. "[" .. delta .. "]" -- remove me + status = status .. " [" .. delta .. "]" -- remove me end end if csarManager.vectoring then @@ -841,9 +856,9 @@ function csarManager.doStatusCarrying(args) report = report .. "\n".. i .. ") " .. evacMission.name if evacMission.expires then delta = math.floor ((evacMission.expires - now) / 60) - if delta > 20 then - report = report .. " is hurt but stable" - elseif delta > 10 then + if delta > 30 then + report = report .. " is hurt and stable" + elseif delta > 15 then report = report .. " is badly hurt" else report = report .. " is in critical condition" -- or 'beat up, but will live' @@ -1438,7 +1453,11 @@ function csarManager.readCSARZone(theZone) -- add to list of startable csar if theZone.startCSAR then - csarManager.addCSARZone(theZone) + if persistence and persistence.hasDate then + -- we load data instead of spawning on start + else + csarManager.addCSARZone(theZone) + end end if (not deferred) then @@ -1592,12 +1611,88 @@ function csarManager.readConfigZone() local typeArray = dcsCommon.splitString(hTypes, ",") typeArray = dcsCommon.trimArray(typeArray) csarManager.rescueTypes = typeArray + + if theZone:hasProperty("timeLimit") then + local tmin, tmax = theZone:getPositiveRangeFromZoneProperty("timeLimit", 1) + csarManager.timeLimit = {tmin, tmax} + else + csarManager.timeLimit = nil + end if csarManager.verbose then trigger.action.outText("+++csar: read config", 30) end end +-- +-- Save and Load Data +-- +function csarManager.saveData() + local now = timer.getTime() + local theData = {} + local missions = {} + -- gather dater from all currently open missions and + -- place them in a new array + for idx, aMission in pairs(csarManager.openMissions) do + m = {} + m.point = aMission.zone:getPoint() + m.side = aMission.side + m.freq = aMission.freq + m.name = aMission.name + m.score = aMission.score + if aMission.expires then + remains = (aMission.expires - now) / 60 -- limit in minutes! + m.timeLimit = {remains, remains} + end + table.insert(missions, m) + end + theData.missions = missions + theData.missionID = csarManager.missionID + + return theData, csarManager.sharedData +end + +function csarManager.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("csarManager", csarManager.sharedData) + if not theData then + if csarManager.verbose then + trigger.action.outText("+++csarManager: no save data received, skipping.", 30) + end + return + end + if theData.missionID then + csarManager.missionID = theData.missionID + end + if theData.missions then + for idx, m in pairs(theData.missions) do + -- csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, timeLimit, mapMarker, inRadius, parashootUnit) + if m.timeLimit and csarManager.verbose then + trigger.action.outText("+++csar: loadData - timelimit of [" .. m.timeLimit[1] .. "," .. m.timeLimit[2] .. "] read for csar <" .. m.name .. ">", 30) + end + local theMission = csarManager.createCSARMissionData( + m.point, -- point, + m.side, -- theSide, + m.freq, -- freq, + m.name, -- name, + nil, -- numCrew, + m.timeLimit, -- timeLimit, can be nil or {lower, upper} + nil, --mapMarker, + 0.1, -- inRadius, + nil -- parashootUnit) + ) + theMission.score = m.score + csarManager.addMission(theMission) + if csarManager.verbose then + trigger.action.outText("+++csarM (persitence): restored csar mission <" .. m.name .. ">", 30) + end + end + end +end + +-- +-- Start +-- function csarManager.start() -- make sure we have loaded all relevant libraries @@ -1624,6 +1719,16 @@ function csarManager.start() csarManager.setCommsMenu(aUnit) end + -- connect to persistence if it exists + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = csarManager.saveData + persistence.registerModule("csarManager", callbacks) + -- now load my data + csarManager.loadData() + end + -- start updating and track all helicopters in the air against missions csarManager.update() diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index a248068..e42ea94 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "3.0.5" +dcsCommon.version = "3.0.6" --[[-- VERSION HISTORY 3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false - point2text new intsOnly option @@ -18,6 +18,9 @@ dcsCommon.version = "3.0.5" - new getFirstItem() - arrayContainsString() can handle dicts - new pointXpercentYdegOffAB() +3.0.6 - new arrayContainsStringCaseInsensitive() +3.0.7 - fixed small bug in wildArrayContainsString + --]]-- -- dcsCommon is a library of common lua functions @@ -2093,7 +2096,7 @@ end if not caseSensitive then theString = string.upper(theString) end local wildIn = dcsCommon.stringEndsWith(theString, "*") - if wildIn then dcsCommon.removeEnding(theString, "*") end + if wildIn then theString = dcsCommon.removeEnding(theString, "*") end for idx, theElement in pairs(theArray) do -- i = 1, #theArray do if not caseSensitive then theElement = string.upper(theElement) end local wildEle = dcsCommon.stringEndsWith(theElement, "*") @@ -2132,6 +2135,19 @@ end end return false end + + function dcsCommon.arrayContainsStringCaseInsensitive(theArray, theString) -- case insensitive + if not theArray then return false end + if not theString then return false end + if type(theArray) ~= "table" then + trigger.action.outText("***arrayContainsStringCI: theArray is not type but <" .. type(theArray) .. ">", 30) + end + theString = string.upper(theString) + for idx, item in pairs(theArray) do + if string.upper(item) == theString then return true end + end + return false + end function dcsCommon.splitString(inputstr, sep) if sep == nil then diff --git a/modules/income.lua b/modules/income.lua index 949e1f5..17a63c0 100644 --- a/modules/income.lua +++ b/modules/income.lua @@ -35,25 +35,40 @@ function income.update() -- schedule next round timer.scheduleFunction(income.update, {}, timer.getTime() + income.interval) + local neuI, redI, blueI = income.neutral, income.red, income.blue -- base income - bank.addFunds(0, income.neutral) - bank.addFunds(1, income.red) - bank.addFunds(2, income.blue) +-- bank.addFunds(0, income.neutral) +-- bank.addFunds(1, income.red) +-- bank.addFunds(2, income.blue) + for idx, theZone in pairs(income.sources) do - bank.addFunds(0, income.getIncomeForZoneAndCoa(theZone, 0)) - bank.addFunds(1, income.getIncomeForZoneAndCoa(theZone, 1)) - bank.addFunds(2, income.getIncomeForZoneAndCoa(theZone, 2)) + local ni = income.getIncomeForZoneAndCoa(theZone, 0) + local ri = income.getIncomeForZoneAndCoa(theZone, 1) + local bi = income.getIncomeForZoneAndCoa(theZone, 2) + redI = redI + ri + blueI = blueI + bi + neuI = neuI + ni end + + bank.addFunds(0, neuI) + bank.addFunds(1, redI) + bank.addFunds(2, blueI) + if income.announceTicks then -- trigger.action.outText(income.tickMessage, 30) local has, balance = bank.getBalance(0) - trigger.action.outTextForCoalition(0, "\n" .. income.tickMessage .. "\nNew balance: §" .. balance .. "\n", 30) + local tick = string.gsub(income.tickMessage, "", neuI) + trigger.action.outTextForCoalition(0, "\n" .. tick .. "\nNew balance: §" .. balance .. "\n", 30) + has, balance = bank.getBalance(1) - trigger.action.outTextForCoalition(1, "\n" .. income.tickMessage .. "\nNew balance: §" .. balance .. "\n", 30) + tick = string.gsub(income.tickMessage, "", redI) + trigger.action.outTextForCoalition(1, "\n" .. tick .. "\nNew balance: §" .. balance .. "\n", 30) + has, balance = bank.getBalance(2) - trigger.action.outTextForCoalition(2, "\n" .. income.tickMessage .. "\nNew balance: §" .. balance .. "\n", 30) + tick = string.gsub(income.tickMessage, "", blueI) + trigger.action.outTextForCoalition(2, "\n" .. tick .. "\nNew balance: §" .. balance .. "\n", 30) end end @@ -71,7 +86,7 @@ function income.readConfigZone() income.neutral = theZone:getNumberFromZoneProperty ("neutral", income.base) income.interval = theZone:getNumberFromZoneProperty("interval", 10 * 60) -- every 10 minutes - income.tickMessage = theZone:getStringFromZoneProperty("tickMessage", "New funds from income available.") + income.tickMessage = theZone:getStringFromZoneProperty("tickMessage", "New funds from income available: §") income.announceTicks = theZone:getBoolFromZoneProperty("announceTicks", true) income.verbose = theZone.verbose end diff --git a/modules/jtacGrpUI.lua b/modules/jtacGrpUI.lua index 84bb1f1..ec739f2 100644 --- a/modules/jtacGrpUI.lua +++ b/modules/jtacGrpUI.lua @@ -6,8 +6,6 @@ jtacGrpUI.requiredLibs = { "cfxGroundTroops", } --[[-- VERSION HISTORY - - 1.0.2 - also include idling JTACS - - add positional info when using owned zones - 2.0.0 - dmlZones - sanity checks upon load - eliminated cfxPlayer dependence @@ -21,14 +19,6 @@ jtacGrpUI.requiredLibs = { jtacGrpUI.groupConfig = {} -- all inited group private config data, indexed by group name. jtacGrpUI.simpleCommands = true -- if true, f10 other invokes directly --- --- C O N F I G H A N D L I N G --- ============================= --- --- Each group has their own config block that can be used to --- store group-private data and configuration items. --- - function jtacGrpUI.resetConfig(conf) end diff --git a/modules/milGround.lua b/modules/milGround.lua new file mode 100644 index 0000000..500953f --- /dev/null +++ b/modules/milGround.lua @@ -0,0 +1,126 @@ +milGround = {} +milGround.version = "0.0.0" +milGround.requiredLibs = { + "dcsCommon", + "cfxZones", + "cfxMX", + "cloneZones", +} +milGround.ups = 0.5 -- every 2 seconds is enough +milGround.zones = {} + +function milGround.addMilGroundZone(theZone) + milGround.zones[theZone.name] = theZone +end + +-- +-- Reading zones +-- +function milGround.readMilGroundZone(theZone) + -- first, check if this zone is also a cloner + if not theZone:hasProperty("cloner") then + trigger.action.outText("mGnd: WARNING: milGround zone <" .. theZone.name .. "> has no 'cloner' interface, will fail!", 30) + end + -- now get the target zone. it's inside the milGround property + local tzn = theZone:getStringFromZoneProperty("milGround", "cfxNone") + local tz = cfxZones.getZoneByName(tzn) + if not tz then + trigger.action.outText("mGnd: target zone <" .. tzn .. "> not found for milGroundZone <" .. theZone.name .. ">, will fail!", 30) + end + theZone.targetZone = tz + if theZone:hasProperty("coalition") then + theZone.owner = theZone:getCoalitionFromZoneProperty("coalition") + end +end + +-- +-- Update +-- +function milGround.update() + timer.scheduleFunction(milGround.update, {}, timer.getTime() + 1/milGround.ups) + + for zName, theZone in pairs(milGround.zones) do + -- synch owner and coa + local mo = theZone.masterowner + if mo then + theZone.owner = mo.owner + if theZone.isDynamic then + theZone.coa = theZone.owner + end + end + end +end + +-- +-- config +-- +function milGround.readConfigZone() + local theZone = cfxZones.getZoneByName("milGroundConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("milGroundConfig") + end + milGround.verbose = theZone.verbose +end + +-- +-- API +-- +function milGround.getAttackersForEnemiesOfCoa(coa, addNeutral) + -- return all milGround zones that attack zones that belong to + -- the enemy of coa + local theOtherSide = dcsCommon.getEnemyCoalitionFor(coa) + local attackers = {} + for zName, theZone in pairs(milGround.zones) do + local tz = theZone.targetZone + if tz.owner ~= coa then + if addNeutral then + table.insert(attackers, theZone) + else + if tz.owner == theOtherSide then + table.insert(attackers, theZone) + end + end + end + end + return attackers +end + +function milGround.startAttackFrom(theZone) + cloneZones.spawnWithCloner(theZone) -- that's all, folx +end + +-- +-- start up +-- +function milGround.start() + -- lib check + if not dcsCommon.libCheck then + trigger.action.outText("cfx mil ground requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx mil ground", milGround.requiredLibs) then + return false + end + + -- read config + milGround.readConfigZone() + + -- process milGround Zones + local attrZones = cfxZones.getZonesWithAttributeNamed("milGround") + for k, aZone in pairs(attrZones) do + milGround.readMilGroundZone(aZone) -- process attributes + milGround.addMilGroundZone(aZone) -- add to list + end + + -- start update in 5 seconds + timer.scheduleFunction(milGround.update, {}, timer.getTime() + 1/milGround.ups) + + -- say hi + trigger.action.outText("milGround v" .. milGround.version .. " started.", 30) + return true +end + +if not milGround.start() then + trigger.action.outText("milGround failed to start.", 30) + milGround = nil +end \ No newline at end of file diff --git a/modules/milHelo.lua b/modules/milHelo.lua index 866895c..04a3d96 100644 --- a/modules/milHelo.lua +++ b/modules/milHelo.lua @@ -1,5 +1,5 @@ milHelo = {} -milHelo.version = "0.0.0" +milHelo.version = "1.0.0" milHelo.requiredLibs = { "dcsCommon", "cfxZones", @@ -7,7 +7,7 @@ milHelo.requiredLibs = { } milHelo.zones = {} milHelo.targetKeywords = { - "milTarget", -- my own + "milTarget", -- my own zone "camp", -- camps "airfield", -- airfields "FARP", -- FARPzones @@ -74,6 +74,19 @@ function milHelo.readMilHeloZone(theZone) -- process attributes theZone.msnType = "cas" end + -- see if our ownership is tied to a master + -- adds dynamic coalition capability + if theZone:hasProperty("masterOwner") then + local mo = theZone:getStringFromZoneProperty("masterOwner") + local mz = cfxZones.getZoneByName(mo) + if not mz then + trigger.action.outText("+++milH: WARNING: Master Owner <" .. mo .. "> for zone <" .. theZone.name .. "> does not exist!", 30) + else + theZone.masterOwner = mz + end + theZone.isDynamic = theZone:getBoolFromZoneProperty("dynamic", true) + end + -- get all groups inside me local myGroups, count = milHelo.allGroupsInZoneByData(theZone) theZone.myGroups = myGroups @@ -179,6 +192,9 @@ function milHelo.createROETask(num, roe) end function milHelo.createEngageIZTask(num, theZone) +-- trigger.action.outText("Creating engage in zone task for zone <" .. theZone.name .. ">, marking on map", 30) +-- theZone:drawZone() +-- theZone:drawText("casz - " .. theZone.name, 20) local p = theZone:getPoint() if not num then num = 1 end local task = {} @@ -266,7 +282,8 @@ function milHelo.createCommandTask(theCommand, num) return t end -function milHelo.createTakeOffWP(theZone, engageInZone, engageZone) +function milHelo.createTakeOffWP(theZone, engageInZone, engageZone, ROE) + if not ROE then ROE = 0 end -- wepons free local WP = {} WP.alt = 500 -- theZone.alt WP.alt_type = "BARO" @@ -281,7 +298,7 @@ function milHelo.createTakeOffWP(theZone, engageInZone, engageZone) local tasks = {} local casTask = milHelo.createCASTask(1) tasks[1] = casTask - local roeTask = milHelo.createROETask(2,0) -- 0 = weapons free + local roeTask = milHelo.createROETask(2,ROE) -- 0 = weapons free, 4 = weapon hold tasks[2] = roeTask if engageInZone then if not engageZone then @@ -353,7 +370,7 @@ function milHelo.createLandWP(gName, theZone, targetZone) return toWP end -function milHelo.createOMWCallbackWP(gName, number, pt, alt, speed, action) -- name is group name +function milHelo.createOMWCallbackWP(gName, number, pt, alt, speed, action, ROE) -- name is group name if not action then action = "none" end local omwWP = dcsCommon.createSimpleRoutePointData(pt, alt, speed) omwWP.alt_type = "RADIO" @@ -364,29 +381,23 @@ function milHelo.createOMWCallbackWP(gName, number, pt, alt, speed, action) -- n local ttsk = {} local command = "milHelo.reachedWP('" .. gName .. "', '" .. number .. "', '" .. action .."')" ttsk[1] = milHelo.createCommandTask(command,1) + if ROE then + ttsk[2] = milHelo.createROETask(2, ROE) + end task.params.tasks = ttsk omwWP.task = task return omwWP end --- a point yDegrees off the path from AB, xPercent of the total distance --- between A and B away from A ---[[-- -function milHelo.pointXpercentYdegOffAB(A, B, xPer, yDeg) -- rets xzz point - local bearingRad = dcsCommon.bearingFromAtoB(A, B) - local dist = dcsCommon.dist(A, B) - local deviation = bearingRad + yDeg * 0.0174533 - local newDist = dist * xPer/100 - local newPoint = dcsCommon.pointInDirectionOfPointXYY(deviation, newDist, A) - return newPoint -end ---]]-- function milHelo.spawnForZone(theZone, targetZone) + -- note that each zone only has a single msnType, so zone + -- defines msn type local n = dcsCommon.randomBetween(1, theZone.hCount) local theRawData = dcsCommon.getNthItem(theZone.hGroups, n) local gData = dcsCommon.clone(theRawData) local oName = gData.name + gData.lateActivation = false -- pre-process gData: names, id etc gData.name = dcsCommon.uuid(gData.name) @@ -414,41 +425,59 @@ function milHelo.spawnForZone(theZone, targetZone) trigger.action.outText("Setting up casZ for <" .. theZone.name .. "> to <" .. targetZone.name .. ">", 30) end - local wpTOff = milHelo.createTakeOffWP(theZone, casInZone, targetZone) + local wpTOff = milHelo.createTakeOffWP(theZone, casInZone, targetZone) -- no ROE = weapons free -- depending on mission, create an orbit or land WP local dest = targetZone:getPoint() local B = dest local A = theZone:getPoint() if theZone.msnType == "cas" or theZone.msnType == "patrol" then - dcsCommon.addRoutePointForGroupData(gData, wpTOff) + -- patrol and cas go straight to the target, they do not + -- have an ingress. Meaning: they have the same route + -- profile as 'insert' + wpTOff = milHelo.createTakeOffWP(theZone, casInZone, targetZone, 4) -- 4 = weapons HOLD + dcsCommon.addRoutePointForGroupData(gData, wpTOff) -- wp 1 + + -- on approach, 70% at target, go weapons hot + local apr = dcsCommon.vLerp(A, B, 0.75) + local omw2 = milHelo.createOMWCallbackWP(gName, 2, apr, theZone.alt, theZone.speed, "weapons free", 0) -- wp 2 + dcsCommon.addRoutePointForGroupData(gData, omw2) + + -- possible expandion: if cas, we have an ingress point? local wpDest = milHelo.createOrbitWP(theZone, dest) - dcsCommon.addRoutePointForGroupData(gData, wpDest) - local retPt = milHelo.createLandWP(gName, theZone, theZone) - dcsCommon.addRoutePointForGroupData(gData, retPt) - --dcsCommon.dumpVar2Str("caser group", gData) + dcsCommon.addRoutePointForGroupData(gData, wpDest) -- wp 3 + --local retPt = milHelo.createLandWP(gName, theZone, theZone) + --dcsCommon.addRoutePointForGroupData(gData, retPt) + local retpt = theZone:getPoint() + local omw4 = milHelo.createOMWCallbackWP(gName, 4, retpt, theZone.alt, theZone.speed, "remove") + dcsCommon.addRoutePointForGroupData(gData, omw4) -- wp 4 elseif theZone.msnType == "casz" then - dcsCommon.addRoutePointForGroupData(gData, wpTOff) + wpTOff = milHelo.createTakeOffWP(theZone, casInZone, targetZone, 4) -- 4 = ROE weapons hold + dcsCommon.addRoutePointForGroupData(gData, wpTOff) -- wp 1 -- go to CAS destination with Engage in Zone active -- we may want to make ingress and egress wp before heading to -- the 'real' CASZ point -- make ingress point, in direction of target, 30 degrees to the right, half distance. local ingress = dcsCommon.pointXpercentYdegOffAB(A, B, math.random(50,80), math.random(20,50)) --local pt = targetZone:getPoint() - local omw1 = milHelo.createOMWCallbackWP(gName, 2, ingress, theZone.alt, theZone.speed, "none") + local omw1 = milHelo.createOMWCallbackWP(gName, 2, ingress, theZone.alt, theZone.speed, "weapons free", 0) -- wp 2 dcsCommon.addRoutePointForGroupData(gData, omw1) local omw2 = milHelo.createOMWCallbackWP(gName, 3, B, theZone.alt, theZone.speed, "none") - dcsCommon.addRoutePointForGroupData(gData, omw2) + dcsCommon.addRoutePointForGroupData(gData, omw2) -- wp 3 -- egress point local egress = dcsCommon.pointXpercentYdegOffAB(B, A, math.random(20, 50), math.random(20,50)) local omw3 = milHelo.createOMWCallbackWP(gName, 4, egress, theZone.alt, theZone.speed, "none") - dcsCommon.addRoutePointForGroupData(gData, omw3) - local retPt = milHelo.createLandWP(gName, theZone, theZone) - dcsCommon.addRoutePointForGroupData(gData, retPt) + dcsCommon.addRoutePointForGroupData(gData, omw3) -- wp 4 + -- return to aerodrome, deallocate + local retpt = theZone:getPoint() + local omw4 = milHelo.createOMWCallbackWP(gName, 5, retpt, theZone.alt, theZone.speed, "remove") + dcsCommon.addRoutePointForGroupData(gData, omw4) -- wp 5 + elseif theZone.msnType == "insert" then local wpDest = milHelo.createLandWP(gName, theZone, targetZone) - dcsCommon.addRoutePointForGroupData(gData, wpTOff) - dcsCommon.addRoutePointForGroupData(gData, wpDest) + dcsCommon.addRoutePointForGroupData(gData, wpTOff) -- wp 1 + dcsCommon.addRoutePointForGroupData(gData, wpDest) -- wp 2 + -- will land and dealloc there after spawning troops end -- make coa a cty @@ -481,6 +510,7 @@ function milHelo.insertTroops(theUnit, targetZone, srcZone) end local gData = dcsCommon.clone(theRawData) + gData.lateActivation = false -- force false -- deploy in ring formation -- remove all routes -- mayhaps prepare for orders and formation @@ -511,7 +541,7 @@ function milHelo.insertTroops(theUnit, targetZone, srcZone) local groupCat = Group.Category.GROUND local theSpawnedGroup = coalition.addGroup(cty, groupCat, gData) - trigger.action.outText("Inserted troops <" .. gName .. ">", 30) + --trigger.action.outText("Inserted troops <" .. gName .. ">", 30) return theSpawnedGroup, gData end @@ -580,12 +610,24 @@ function milHelo.spawnImpostorsFromData(rawData, cat, ctry) end function milHelo.reachedWP(gName, wpNum, action) - trigger.action.outText("MilH group <" .. gName .. " reached wp #" .. wpNum .. ".", 30) + if not action then action = "NIL" end + if milHelo.verbose then + trigger.action.outText("MilH group <" .. gName .. " reached wp #" .. wpNum .. " with action <" .. action .. ">.", 30) + end + if action == "remove" then + theGroup = Group.getByName(gName) + if theGroup and Group.isExist(theGroup) then + if milHelo.verbose then + trigger.action.outText("%%%%%%%%%% removing mil hel <" .. gName .. ">", 30) + end + Group.destroy(theGroup) + end + end end function milHelo.landedCB(who, where, from) -- who group name, where a zone - trigger.action.outText("milhelo landed CB for group <" .. who .. ">", 30) +-- trigger.action.outText("milhelo landed CB for group <" .. who .. ">", 30) -- step 1: remove the flight local theGroup = Group.getByName(who) if theGroup then @@ -601,7 +643,8 @@ function milHelo.landedCB(who, where, from) -- who group name, where a zone local theFlight = milHelo.flights[who] local oName = theFlight.oName local theZone = theFlight.origin - if theZone.msn == "insertion" then + -- note: "insertion" is probably wrong, remove in line below + if theZone.msnType == "insertion" or theZone.msnType == "insert" then -- create a static stand-in for scenery local rawData, cat, ctry = milHelo.getRawDataFromGroupNamed(who, oName) Group.destroy(aGroup) @@ -620,6 +663,17 @@ end -- function milHelo.update() timer.scheduleFunction(milHelo.update, {}, timer.getTime() + 1) + -- update all master owners + for idx, theZone in pairs (milHelo.zones) do + local mo = theZone.masterOwner + if mo then + theZone.owner = mo.owner + if theZone.isDynamic then + theZone.coa = mo.owner + end + end + end + end function milHelo.GCcollected(gName) @@ -672,10 +726,16 @@ function milHelo:onEvent(theEvent) else -- maybe its a return flight if srcZone:pointInZone(p) then - trigger.action.outText("Flight <" .. gName .. "> originating from <" .. srcZone.name .. "> landed back home", 30) +-- trigger.action.outText("Flight <" .. gName .. "> originating from <" .. srcZone.name .. "> landed back home", 30) else - trigger.action.outText("Flight <" .. gName .. "> originating from <" .. srcZone.name .. "> landed OUTSIDE of src or target zone <" .. tgtZone.name .. ">", 30) - end +-- trigger.action.outText("Flight <" .. gName .. "> originating from <" .. srcZone.name .. "> landed OUTSIDE of src or target zone <" .. tgtZone.name .. ">, clearing.", 30) + end + -- remove it now + local theGroup = Group.getByName(gName) + if theGroup and Group.isExist(theGroup) then + Group.destroy(theGroup) + end + end end @@ -690,7 +750,7 @@ function milHelo.getMilSources(side, msnType) -- msnType is optional if side == "blue" then side = 2 end local sources = {} for idx, theZone in pairs(milHelo.zones) do - if theZone.owner == side then -- must be owned by same side + if theZone.coa == side then -- coa must be same side, use masterOwner for dynamism if msnType then if theZone.msnType == msnType then table.insert(sources, theZone) @@ -703,13 +763,18 @@ function milHelo.getMilSources(side, msnType) -- msnType is optional return sources -- an array, NOT dict so we can pickrandom end -function milHelo.getMilTargets(side) -- gets mil targets that DO NOT belong to side +function milHelo.getMilTargets(side, ignoreNeutral) -- gets mil targets that DO NOT belong to side if side == "red" then side = 1 end -- better safe... if side == "blue" then side = 2 end local tgt = {} for idx, theZone in pairs(milHelo.targets) do + -- we use OWNER, not COA here! if theZone.owner ~= side then -- must NOT be owned by same side - table.insert(tgt, theZone) + if ignoreNeutral and theZone.owner == 0 then + else + table.insert(tgt, theZone) + --trigger.action.outText("zone <" .. theZone.name .. "> owned by <" .. theZone.owner .. "> is possible target for coa <" .. side .. ">", 30) + end end end return tgt @@ -730,7 +795,7 @@ end function milHelo.start() -- lib check if not dcsCommon.libCheck then - trigger.action.outText("cfx civ helo requires dcsCommon", 30) + trigger.action.outText("cfx mil helo requires dcsCommon", 30) return false end if not dcsCommon.libCheck("cfx mil helo", milHelo.requiredLibs) then @@ -758,6 +823,9 @@ function milHelo.start() -- start update in 5 seconds timer.scheduleFunction(milHelo.update, {}, timer.getTime() + 1/milHelo.ups) + -- start GC + milHelo.GC() + -- install event handler world.addEventHandler(milHelo) diff --git a/modules/milWings.lua b/modules/milWings.lua new file mode 100644 index 0000000..48b563b --- /dev/null +++ b/modules/milWings.lua @@ -0,0 +1,572 @@ +milWings = {} +milWings.version = "0.9.5" +milWings.requiredLibs = { + "dcsCommon", + "cfxZones", + "cfxMX", + "wingTaskHelper", +} +milWings.zones = {} -- mil wings zones for flights. can have master owner +milWings.targetKeywords = { -- same as mil helo plus x + "wingTarget", -- my own zone + "milTarget", -- milH zones + "camp", -- camps + "airfield", -- airfields + "FARP", -- FARPzones + } + +milWings.targets = {} -- targets for mil wings. can have master owner +-- includes wingTarget and other keywors +milWings.pureTargets = {} -- targets with wingTarget keyword +milWings.seadTargets = {} +milWings.flights = {} -- all currently active mil helo flights +milWings.ups = 1 +milWings.missionTypes = { + "cas", -- standard cas + "cap", -- orbit over zone for duration + "sead", -- engage in zone for target zone's radius + "bomb", +} + +function milWings.addMilWingsZone(theZone) + milWings.zones[theZone.name] = theZone +end + +function milWings.addMilWingsTargetZone(theZone) + milWings.targets[theZone.name] = theZone -- overwrite if duplicate + if theZone:hasProperty("SEAD") then + milWings.seadTargets[theZone.name] = theZone + end + if theZone:hasProperty("wingTarget") then + milWings.pureTargets[theZone.name] = theZone + theZone.wingTargetName = theZone:getStringFromZoneProperty("wingTarget", "<*" .. theZone.name .. ">") + end + +end + +-- +-- Reading / Processing milWings Zones +-- +function milWings.partOfGroupDataInZone(theZone, theUnits) -- move to mx? + local zP = theZone:getDCSOrigin() -- don't use getPoint now. + zP.y = 0 + for idx, aUnit in pairs(theUnits) do + local uP = {} + uP.x = aUnit.x + uP.y = 0 + uP.z = aUnit.y -- !! y-z + if theZone:pointInZone(uP) then return true end + end + return false +end + +function milWings.allGroupsInZoneByData(theZone) -- move to MX? + local theGroupsInZone = {} + local count = 0 + for groupName, groupData in pairs(cfxMX.groupDataByName) do + if groupData.units then + if milWings.partOfGroupDataInZone(theZone, groupData.units) then + theGroupsInZone[groupName] = groupData -- DATA! work on clones! + count = count + 1 + if theZone.verbose then + trigger.action.outText("+++milH: added group <" .. groupName .. "> for zone <" .. theZone.name .. ">", 30) + end + end + end + end + return theGroupsInZone, count +end + +function milWings.readMilWingsZone(theZone) -- process attributes + theZone.msnType = string.lower(theZone:getStringFromZoneProperty("milWings", "cas")) + if dcsCommon.arrayContainsString(milWings.missionTypes, theZone.msnType) then + -- great, mission type is known + else + trigger.action.outText("+++milW: zone <" .. theZone.name .. ">: unknown wing mission type <" .. theZone.msnType .. ">, defaulting to 'CAS'", 30) + theZone.msnType = "cas" + end + + -- see if our ownership is tied to a master + -- this adds dynamic coalition capability + if theZone:hasProperty("masterOwner") then + local mo = theZone:getStringFromZoneProperty("masterOwner") + local mz = cfxZones.getZoneByName(mo) + if not mz then + trigger.action.outText("+++milW: WARNING: Master Owner <" .. mo .. "> for zone <" .. theZone.name .. "> does not exist!", 30) + else + theZone.masterOwner = mz + end + theZone.isDynamic = theZone:getBoolFromZoneProperty("dynamic", true) + if theZone.verbose then + trigger.action.outText("milwing target <" .. theZone.name .. "> has masterOwner <" .. theZone.mz.name .. ">", 30) + if theZone.isDynamic then + trigger.action.outText("and coa is dynamically linked to owner.", 30) + end + end + end + + -- get all groups inside me + local myGroups, count = milWings.allGroupsInZoneByData(theZone) + theZone.fGroups = {} + theZone.fCount = 0 + -- sort into ground, helo and fixed + for groupName, data in pairs(myGroups) do + local catRaw = cfxMX.groupTypeByName[groupName] + if theZone.verbose then + trigger.action.outText("Proccing zone <" .. theZone.name .. ">: group <" .. groupName .. "> - type <" .. catRaw .. ">", 30) + end + if catRaw == "plane" then + theZone.fGroups[groupName] = data + theZone.fCount = theZone.fCount + 1 + else + trigger.action.outText("+++milH: ignored group <" .. groupName .. ">: wrong type <" .. catRaw .. ">", 30) + end + end + theZone.coa = theZone:getCoalitionFromZoneProperty("coalition", 0) + theZone.hot = theZone:getBoolFromZoneProperty("hot", false) + theZone.inAir = theZone:getBoolFromZoneProperty("inAir", false) + theZone.speed = theZone:getNumberFromZoneProperty("speed", 220) -- 800 kmh + theZone.alt = theZone:getNumberFromZoneProperty("alt", 6000) -- we are always radar alt + theZone.loiter = theZone:getNumberFromZoneProperty("loiter", 3600) -- 1 hour loiter default + + -- wipe all existing + for groupName, data in pairs(theZone.fGroups) do + local g = Group.getByName(groupName) + if g then + Group.destroy(g) + end + end + if theZone.verbose or milWings.verbose then + trigger.action.outText("+++milW: processed milWings zone <" .. theZone.name .. ">", 30) + end +end + +function milWings.readMilWingsTargetZone(theZone) + -- can also be "camp", "farp", "airfield" + theZone.wingRadius = theZone:getNumberFromZoneProperty("wingRadius", theZone.radius) + if (not theZone.isCircle) and (not theZone:hasProperty("wingRadius")) then + -- often when we have a camp there is no cas radius, use 60km + -- and zone is ploygonal + if theZone.verbose then + trigger.action.outText("+++milH: Warning - milH target zone <" .. theZone.name .. "> is polygonal and has no CAS radius attribute. Defaulting to 80km", 30) + end +-- theZone.casRadius = 80000 + theZone.wingRadius = 80000 + end + + if theZone:hasProperty("wingTypes") then -- if present, else all good + theZone.wingTypes = theZone:getListFromZoneProperty("wingTypes", "empty") + end + + if theZone.verbose or milWings.verbose then + trigger.action.outText("+++milH: processed milWings TARGET zone <" .. theZone.name .. ">", 30) + end + + if theZone:hasProperty("masterOwner") then + local mo = theZone:getStringFromZoneProperty("masterOwner") + local mz = cfxZones.getZoneByName(mo) + if not mz then + trigger.action.outText("+++milW: WARNING: Master Owner <" .. mo .. "> for zone <" .. theZone.name .. "> does not exist!", 30) + else + theZone.masterOwner = mz + end + end +end + + +-- +-- creating flights +-- +function milWings.createTakeOffWayPoint(theZone, targetZone) + local wp = {} + wp.alt = theZone.alt + wp.action = "Turning Point" -- default. overrides hot + wp.type = "Turning Point" + if not theZone.inAir then + if theZone.hot then + wp.action = "From Parking Area Hot" + wp.type = "TakeOffParkingHot" + else + wp.action = "From Parking Area" + wp.type = "TakeOffParking" + end + wp.alt = 0 + wp.speed = 0 + local af = dcsCommon.getClosestAirbaseTo(theZone:getPoint(), 0) +-- trigger.action.outText("closest airfield for this flight is <" .. af:getName() .. ">", 30) + wp.airdromeId = af:getID() + end +-- trigger.action.outText("flight has action <" .. wp.action .. "> and type <" .. wp.type .. ">", 30) + + wp.speed = theZone.speed + local p = theZone:getPoint() + wp.x = p.x + wp.y = p.z + wp.formation_template= "" + if theZone.msnType == "cas" then + wp.task = dcsCommon.clone(wingTaskHelper.casTOTask) + -- now simply change some bits from the template + p = targetZone:getPoint() + wp.task.params.tasks[8].params.x = p.x + wp.task.params.tasks[8].params.y = p.z + if targetZone.wingRadius then -- should ALWAYS be true + wp.task.params.tasks[8].params.zoneRadius = targetZone.wingRadius + else + wp.task.params.tasks[8].params.zoneRadius = 80000 + trigger.action.outText("WARNING: creating CAS flight <" .. theZone.name .. "> with no radius for target zone <" .. targetZone.name .. ">", 30) + end + elseif theZone.msnType == "cap" then + wp.task = dcsCommon.clone(wingTaskHelper.capTOTask) + p = targetZone:getPoint() + wp.task.params.tasks[6].params.x = p.x + wp.task.params.tasks[6].params.y = p.z +-- wp.task.params.tasks[6].params.zoneRadius = theZone.capRadius + if targetZone.wingRadius then -- should ALWAYS be true + wp.task.params.tasks[6].params.zoneRadius = targetZone.wingRadius + else + wp.task.params.tasks[6].params.zoneRadius = 80000 +-- trigger.action.outText("WARNING: creating CAP flight <" .. theZone.name .. "> with no radius for target zone <" .. targetZone.name .. ">", 30) + end + elseif theZone.msnType == "sead" then + wp.task = dcsCommon.clone(wingTaskHelper.seadTOTask) + elseif theZone.msnType == "bomb" then + wp.task = dcsCommon.clone(wingTaskHelper.bombTOTask) + else + trigger.action.outText("milW: unknown msnType <" .. theZone.msnType .. "> in zone <" .. theZone.name .. ">", 30) + end + + return wp +end + +function milWings.createActionWaypoint(theZone, targetZone) + local p = targetZone:getPoint() + local wp = dcsCommon.createSimpleRoutePointData(p, theZone.alt, theZone.speed) + if theZone.msnType == "bomb" then + local task = dcsCommon.clone(wingTaskHelper.bombActionTask) + task.params.tasks[1].params.x = p.x + task.params.tasks[1].params.y = p.z + task.params.tasks[1].params.altitude = theZone.alt + task.params.tasks[1].params.speed = theZone.speed + wp.task = task + end + return wp +end + +function milWings.createCommandTask(theCommand, num) + if not num then num = 1 end + local t = {} + t.enabled = true + t.auto = false + t.id = "WrappedAction" + t.number = num + local params = {} + t.params = params + local action = {} + params.action = action + action.id = "Script" + local p2 = {} + action.params = p2 + p2.command = theCommand + return t +end + +function milWings.createCallbackWP(gName, number, pt, alt, speed, action) -- name is group name + if not action then action = "none" end + local omwWP = dcsCommon.createSimpleRoutePointData(pt, alt, speed) + omwWP.alt_type = "BARO" + -- create a command waypoint + local task = {} + task.id = "ComboTask" + task.params = {} + local ttsk = {} + local command = "milWings.inFlightCB('" .. gName .. "', '" .. number .. "', '" .. action .."')" + ttsk[1] = milWings.createCommandTask(command,1) + task.params.tasks = ttsk + omwWP.task = task + return omwWP +end + +function milWings.spawnForZone(theZone, targetZone) + -- pick one of the flight groups + if not theZone.fCount or theZone.fCount < 1 then + trigger.action.outText("+++milW: WARNING - no f-groups in zone <" .. theZone.name .. "> at spawnForZone", 30) + return nil, nil + end + local n = dcsCommon.randomBetween(1, theZone.fCount) + local theRawData = dcsCommon.getNthItem(theZone.fGroups, n) + local gData = dcsCommon.clone(theRawData) + if not gData then + trigger.action.outText("+++milW: WARNING: NIL gData in spawnForZone for <" .. theZone.name .. ">", 30) + return nil, nil + end + gData.lateActivation = false + + local oName = gData.name + + -- pre-process gData: names, id etc + gData.name = dcsCommon.uuid(gData.name) + local gName = gData.name + for idx, uData in pairs(gData.units) do + uData.name = dcsCommon.uuid(uData.name) + uData.alt = theZone.alt + uData.alt_type = "BARO" + uData.speed = theZone.speed + uData.unitId = nil + end + gData.groupId = nil + + -- set task for group + gData.task = "CAS" -- default + if theZone.msnType == "cap" then + gData.task = "CAP" + elseif theZone.msnType == "sead" then + gData.task = "SEAD" + elseif theZone.msnType == "bomb" then + gData.task = "Ground Attack" + end +-- trigger.action.outText("main task is " .. gData.task, 30) + + -- create route + local route = {} + route.routeRelativeTOT = true + route.points = {} + gData.route = route + + -- create take-off waypoint for this flight + local wpTOff = milWings.createTakeOffWayPoint(theZone, targetZone) + dcsCommon.addRoutePointForGroupData(gData, wpTOff) + + -- ingress point + local dest = targetZone:getPoint() + local B = dest + local A = theZone:getPoint() + local ingress = dcsCommon.pointXpercentYdegOffAB(A, B, math.random(50,80), math.random(20,50)) + local omwWP = dcsCommon.createSimpleRoutePointData(ingress, theZone.alt, theZone.speed) + dcsCommon.addRoutePointForGroupData(gData, omwWP) + + -- action waypoint + local awp = milWings.createActionWaypoint(theZone, targetZone) + dcsCommon.addRoutePointForGroupData(gData, awp) + + -- egress + local egress = dcsCommon.pointXpercentYdegOffAB(B, A, math.random(20, 50), math.random(20,50)) + local egWP = dcsCommon.createSimpleRoutePointData(egress, theZone.alt, theZone.speed) + dcsCommon.addRoutePointForGroupData(gData, egWP) + + -- maybe add another to safety and then dealloc? + local final = milWings.createCallbackWP(gData.name, 4, theZone:getPoint(), theZone.alt, theZone.speed, "delete") + dcsCommon.addRoutePointForGroupData(gData, final) + + -- spawn and return + local cty = dcsCommon.getACountryForCoalition(theZone.coa) + -- spawn + local groupCat = Group.Category.AIRPLANE + local theSpawnedGroup = coalition.addGroup(cty, groupCat, gData) + local theFlight = {} + theFlight.oName = oName + theFlight.spawn = theSpawnedGroup + theFlight.origin = theZone + theFlight.destination = targetZone + milWings.flights[gName] = theFlight --theSpawnedGroup + return theSpawnedGroup, gData +end + +-- +-- Update +-- +function milWings.update() + timer.scheduleFunction(milWings.update, {}, timer.getTime() + 1) + -- update all master owners + for idx, theZone in pairs (milWings.zones) do + local mo = theZone.masterOwner + if mo then + theZone.owner = mo.owner + if theZone.isDynamic then + theZone.coa = mo.owner + end + if theZone.verbose then + trigger.action.outText("Copied master onwer <" .. mo.owner .. "> from <" .. mo.name .. "> to <" .. theZone.name .. ">", 30) + end + end + end + + for idx, theZone in pairs (milWings.targets) do + local mo = theZone.masterOwner + if mo then + theZone.owner = mo.owner + if theZone.verbose then + trigger.action.outText("Copied master onwer <" .. mo.owner .. "> from <" .. mo.name .. "> to <" .. theZone.name .. ">", 30) + end + end + end + +end + +-- +-- Event Handler +-- +function milWings:onEvent(theEvent) +if not theEvent then return end + if not theEvent.initiator then return end + local theUnit = theEvent.initiator + if not theUnit.getGroup then return end + local theGroup = theUnit:getGroup() + if not theGroup then return end + local gName = theGroup:getName() + if not gName then return end + local theFlight = milWings.flights[gName] + if not theFlight then return end -- none of ours + + local id = theEvent.id + if id == 4 then + -- flight landed -- milFlights currently do not land + -- except later transport flights -- we'll deal with those + -- later + +-- trigger.action.outText("+++milW: flight <> landed (and removed)", 30) + if Group.isExist(theGroup) then + -- maybe schedule in a few seconds? + Group.destroy(theGroup) + end + end -- if landed +end +-- +-- callback from the flight +-- +function milWings.inFlightCB(gName) +-- trigger.action.outText("*****===***** callback in-flight for group <" .. gName .. ">", 30) + local theGroup = Group.getByName(gName) + if theGroup and Group.isExist(theGroup) then Group.destroy(theGroup) end + end + +-- +-- GC +-- +function milWings.GCcollected(gName) + -- do some housekeeping? + if milWings.verbose then + trigger.action.outText("removed MIL flight <" .. gName .. ">", 30) + end +end + +function milWings.GC() + timer.scheduleFunction(milWings.GC, {}, timer.getTime() + 5) + local filtered = {} + for gName, theFlight in pairs(milWings.flights) do + local theGroup = Group.getByName(gName) + if theGroup and Group.isExist(theGroup) then + -- all fine, keep it + filtered[gName] = theFlight + else + milWings.GCcollected(gName) + end + end + milWings.flights = filtered +end + +-- +-- API +-- +function milWings.getMilWingSources(side, msnType) -- msnType is optional + if side == "red" then side = 1 end -- better safe... + if side == "blue" then side = 2 end + local sources = {} + for idx, theZone in pairs(milWings.zones) do + if theZone.coa == side then -- coa must be same side, use masterOwner for dynamism + if msnType then + if theZone.msnType == msnType then + table.insert(sources, theZone) + end + else + table.insert(sources, theZone) + end + end + end + return sources -- an array, NOT dict so we can pickrandom +end + +function milWings.getMilWingTargets(side, msnType, ignoreNeutral, pure) -- gets mil targets that DO NOT belong to side +-- enter with side = -1 to get all + local source = milWings.targets + if pure then source = milWings.pureTargets end + if side == "red" then side = 1 end -- better safe... + if side == "blue" then side = 2 end + local tgt = {} + for idx, theZone in pairs(source) do + if theZone.owner ~= side then -- must NOT be owned by same side + if ignoreNeutral and theZone.owner == 0 then + -- neutral ignored + else + -- now see if we need to filter by zone's msnType + if msnType then + if theZone.wingTypes and dcsCommon.arrayContainsStringCaseInsensitive(theZone.wingTypes, msnType) then + table.insert(tgt, theZone) + end + else + table.insert(tgt, theZone) + end + end + end + end + return tgt +end + +-- +-- config +-- +function milWings.readConfigZone() + local theZone = cfxZones.getZoneByName("milWingsConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("milWingsConfig") + end + milWings.verbose = theZone.verbose + +end + +-- +-- Start +-- +function milWings.start() +-- lib check + if not dcsCommon.libCheck then + trigger.action.outText("cfx mil wings requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx mil wings", milWings.requiredLibs) then + return false + end + + -- read config + milWings.readConfigZone() + + -- process milWings Zones + local attrZones = cfxZones.getZonesWithAttributeNamed("milWings") + for k, aZone in pairs(attrZones) do + milWings.readMilWingsZone(aZone) -- process attributes + milWings.addMilWingsZone(aZone) -- add to list + end + + for idx, keyWord in pairs(milWings.targetKeywords) do + attrZones = cfxZones.getZonesWithAttributeNamed(keyWord) + for k, aZone in pairs(attrZones) do + milWings.readMilWingsTargetZone(aZone) -- process attributes + milWings.addMilWingsTargetZone(aZone) -- add to list + end + end + + -- start update in 5 seconds + timer.scheduleFunction(milWings.update, {}, timer.getTime() + 1/milWings.ups) + + timer.scheduleFunction(milWings.GC, {}, timer.getTime() + 1/milWings.ups * 5) + -- install event handler + world.addEventHandler(milWings) + + -- say hi + trigger.action.outText("milWings v" .. milWings.version .. " started.", 30) + return true +end + +if not milWings.start() then + trigger.action.outText("milWings failed to start.", 30) + milWings = nil +end diff --git a/modules/ownedZones.lua b/modules/ownedZones.lua index c08888a..da61ed4 100644 --- a/modules/ownedZones.lua +++ b/modules/ownedZones.lua @@ -1,5 +1,5 @@ cfxOwnedZones = {} -cfxOwnedZones.version = "2.3.0" +cfxOwnedZones.version = "2.3.1" cfxOwnedZones.verbose = false cfxOwnedZones.announcer = true cfxOwnedZones.name = "cfxOwnedZones" @@ -42,6 +42,7 @@ cfxOwnedZones.name = "cfxOwnedZones" - per-zone local numkeep - title attribute - code clean-up +2.3.1 - restored getNearestOwnedZoneToPoint --]]-- cfxOwnedZones.requiredLibs = { "dcsCommon", @@ -250,7 +251,7 @@ function cfxOwnedZones.zoneConquered(aZone, theSide, formerOwner) -- 0 = neutral elseif theSide == 0 then who = "NEUTRAL" end aZone.owner = theSide -- just to be sure - if aZone.announcer then + if cfxOwnedZones.announcer or aZone.announcer then if theSide == 0 then trigger.action.outText(aZone.name .. " has become NEUTRAL", 30) else @@ -632,6 +633,7 @@ end -- collect zones can filter owned zones. -- by default it filters all zones that are in water -- includes all managed-owner zones +-- called from external sources function cfxOwnedZones.collectZones(mode) if not mode then mode = "land" end if mode == "land" then @@ -651,6 +653,12 @@ function cfxOwnedZones.collectZones(mode) end end +-- getNearestOwnedZoneToPoint invoked by heloTroops +function cfxOwnedZones.getNearestOwnedZoneToPoint(p) + local allZones = cfxOwnedZones.collectZones() + return cfxZones.getClosestZone(p, allZones) +end + -- getNearestEnemyOwnedZone invoked by cfxGroundTroops function cfxOwnedZones.getNearestEnemyOwnedZone(theZone, targetNeutral) if not targetNeutral then targetNeutral = false else targetNeutral = true end diff --git a/modules/playerScore.lua b/modules/playerScore.lua index 13fe13f..be1d492 100644 --- a/modules/playerScore.lua +++ b/modules/playerScore.lua @@ -14,7 +14,7 @@ cfxPlayerScore.firstSave = true -- to force overwrite 3.0.1 - cleanup 3.0.2 - interface with ObjectDestructDetector for scoring scenery objects 3.1.0 - shared data for persistence - 3.2.0 - integration with bank + 3.2.0 - integration with bank --]]-- cfxPlayerScore.requiredLibs = { @@ -1023,6 +1023,7 @@ function cfxPlayerScore.scheduledAward(args) cfxPlayerScore.coalitionScore[playerSide] = cfxPlayerScore.coalitionScore[playerSide] + theScore.scoreaccu if bank and bank.addFunds then bank.addFunds(playerSide, cfxPlayerScore.score2finance * theScore.scoreaccu) + desc = desc .. "(transferred §" .. cfxPlayerScore.score2finance * theScore.scoreaccu .. " to funding)\n" end theScore.scoreaccu = 0 hasAward = true @@ -1360,14 +1361,21 @@ function cfxPlayerScore.update() -- score! cfxPlayerScore.coalitionScore[coa] = cfxPlayerScore.coalitionScore[coa] + cfxPlayerScore.blueTriggerScore[tName] cfxPlayerScore.blueTriggerFlags[tName] = newVal - -- bank it if exists - if bank and bank.addFunds then - bank.addFunds(coa, cfxPlayerScore.score2finance * cfxPlayerScore.blueTriggerScore[tName]) - end + if cfxPlayerScore.announcer then trigger.action.outTextForCoalition(coa, "BLUE goal [" .. tName .. "] achieved, new BLUE coalition score is " .. cfxPlayerScore.coalitionScore[coa], 30) trigger.action.outSoundForCoalition(coa, cfxPlayerScore.scoreSound) end + + -- bank it if exists + local amount + if bank and bank.addFunds then + amount = cfxPlayerScore.score2finance * cfxPlayerScore.blueTriggerScore[tName] + bank.addFunds(coa, amount) + if cfxPlayerScore.announcer then + trigger.action.outTextForCoalition(coa, "Transferred §" .. amount .. " to funds.", 30) + end + end end end end @@ -1380,13 +1388,23 @@ function cfxPlayerScore.update() cfxPlayerScore.coalitionScore[coa] = cfxPlayerScore.coalitionScore[coa] + cfxPlayerScore.redTriggerScore[tName] cfxPlayerScore.redTriggerFlags[tName] = newVal - if bank and bank.addFunds then - bank.addFunds(coa, cfxPlayerScore.score2finance * cfxPlayerScore.blueTriggerScore[tName]) - end + --if bank and bank.addFunds then + -- bank.addFunds(coa, cfxPlayerScore.score2finance * cfxPlayerScore.blueTriggerScore[tName]) + --end if cfxPlayerScore.announcer then trigger.action.outTextForCoalition(coa, "RED goal [" .. tName .. "] achieved, new RED coalition score is " .. cfxPlayerScore.coalitionScore[coa], 30) trigger.action.outSoundForCoalition(coa, cfxPlayerScore.scoreSound) end + + -- bank it if exists + local amount + if bank and bank.addFunds then + amount = cfxPlayerScore.score2finance * cfxPlayerScore.redTriggerScore[tName] + bank.addFunds(coa, amount) + if cfxPlayerScore.announcer then + trigger.action.outTextForCoalition(coa, "Transferred §" .. amount .. " to funds.", 30) + end + end end end end diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 65f89e0..85a1170 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -1,5 +1,5 @@ radioMenu = {} -radioMenu.version = "2.2.1" +radioMenu.version = "2.3.0" radioMenu.verbose = false radioMenu.ups = 1 radioMenu.requiredLibs = { @@ -19,6 +19,8 @@ radioMenu.menus = {} 2.1.1 - outMessage now works correctly 2.2.0 - clean-up 2.2.1 - corrected ackD + 2.3.0 - added wildcard "*" ability for group name match + - added ackASnd .. ackDSnd sounds as options --]]-- function radioMenu.addRadioMenu(theZone) @@ -114,17 +116,35 @@ function radioMenu.filterPlayerIDForGroup(theZone) end for idx, gName in pairs(allGroups) do + -- if gName ends in wildcard "*" we process differently gName = dcsCommon.trim(gName) - local theGroup = cfxMX.playerGroupByName[gName] - if theGroup then - local gID = theGroup.groupId - table.insert(theIDs, gID) - if theZone.verbose or radioMenu.verbose then - trigger.action.outText("+++menu: Player Group <" .. gName .. "> found: <" .. gID .. ">", 30) + if dcsCommon.stringEndsWith(gName, "*") then + -- we must check all group names if they start with the + -- the same root. WARNING: CASE-SENSITIVE!!!! + gName = dcsCommon.removeEnding(gName, "*") + for mxName, theGroupData in pairs(cfxMX.playerGroupByName) do + if dcsCommon.stringStartsWith(mxName, gName) then + -- group match, install menu + local gID = theGroupData.groupId + table.insert(theIDs, gID) + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: WILDCARD Player Group <" .. gName .. "*> matched with <" .. mxName .. ">: gID = <" .. gID .. ">", 30) + end + else + end end else - trigger.action.outText("+++menu: Player Group <" .. gName .. "> does not exist", 30) - end + local theGroup = cfxMX.playerGroupByName[gName] + if theGroup then + local gID = theGroup.groupId + table.insert(theIDs, gID) + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: Player Group <" .. gName .. "> found: <" .. gID .. ">", 30) + end + else + trigger.action.outText("+++menu: Player Group <" .. gName .. "> does not exist", 30) + end + end end return theIDs @@ -272,6 +292,9 @@ function radioMenu.createRadioMenuWithZone(theZone) if theZone:hasProperty("ackA") then theZone.ackA = theZone:getStringFromZoneProperty("ackA", "Acknowledged: A") end + if theZone:hasProperty("ackASnd") then + theZone.ackASnd = theZone:getStringFromZoneProperty("ackASnd", "") + end theZone.itemBChosen = theZone:getStringFromZoneProperty("B!", "*") theZone.cooldownB = theZone:getNumberFromZoneProperty("cooldownB", 0) @@ -282,6 +305,9 @@ function radioMenu.createRadioMenuWithZone(theZone) if theZone:hasProperty("ackB") then theZone.ackB = theZone:getStringFromZoneProperty("ackB", "Acknowledged: B") end + if theZone:hasProperty("ackBSnd") then + theZone.ackBSnd = theZone:getStringFromZoneProperty("ackBSnd", "") + end theZone.itemCChosen = theZone:getStringFromZoneProperty("C!", "*") theZone.cooldownC = theZone:getNumberFromZoneProperty("cooldownC", 0) @@ -292,6 +318,9 @@ function radioMenu.createRadioMenuWithZone(theZone) if theZone:hasProperty("ackC") then theZone.ackC = theZone:getStringFromZoneProperty("ackC", "Acknowledged: C") end + if theZone:hasProperty("ackCSnd") then + theZone.ackCSnd = theZone:getStringFromZoneProperty("ackCSnd", "") + end theZone.itemDChosen = theZone:getStringFromZoneProperty("D!", "*") theZone.cooldownD = theZone:getNumberFromZoneProperty("cooldownD", 0) @@ -302,6 +331,9 @@ function radioMenu.createRadioMenuWithZone(theZone) if theZone:hasProperty("ackD") then theZone.ackD = theZone:getStringFromZoneProperty("ackD", "Acknowledged: D") end + if theZone:hasProperty("ackDSnd") then + theZone.ackDSnd = theZone:getStringFromZoneProperty("ackDSnd", "") + end if theZone:hasProperty("removeMenu?") then theZone.removeMenu = theZone:getStringFromZoneProperty( "removeMenu?", "*") @@ -395,6 +427,7 @@ function radioMenu.doMenuX(args) local theFlag = theZone.itemAChosen local outVal = theZone.outValA local ack = theZone.ackA + local ackSnd = theZone.ackASnd -- decode A..X if theItemIndex == "B"then @@ -403,18 +436,21 @@ function radioMenu.doMenuX(args) theFlag = theZone.itemBChosen outVal = theZone.outValB ack = theZone.ackB + ackSnd = theZone.ackBSnd elseif theItemIndex == "C" then cd = radioMenu.cdByGID(theZone.mcdC, theZone, theGroup) -- theZone.mcdC busy = theZone.busyC theFlag = theZone.itemCChosen outVal = theZone.outValC ack = theZone.ackC + ackSnd = theZone.ackCSnd elseif theItemIndex == "D" then cd = radioMenu.cdByGID(theZone.mcdD, theZone, theGroup) -- theZone.mcdD busy = theZone.busyD theFlag = theZone.itemDChosen outVal = theZone.outValD ack = theZone.ackD + ackSnd = theZone.ackDSnd end -- see if we are on cooldown @@ -427,10 +463,13 @@ function radioMenu.doMenuX(args) return else -- see if we have an acknowledge + local gid = theGroup if ack then - local gid = theGroup radioMenu.radioOutMsg(ack, gid, theZone) end + if ackSnd then + trigger.action.outSoundForGroup(gid, ackSnd) + end end -- set new cooldown -- needs own decoder A..X diff --git a/modules/sweeper.lua b/modules/sweeper.lua new file mode 100644 index 0000000..c1a1598 --- /dev/null +++ b/modules/sweeper.lua @@ -0,0 +1,133 @@ +sweeper = {} +sweeper.version = "1.0.0" +sweeper.requiredLibs = { + "dcsCommon", + "cfxZones", +} +-- remove all units that are detected twice in a row in the same +-- zone after a time interval. Used to remove deadlocked units. + +sweeper.zones = {} +sweeper.interval = 5 * 60 -- 5 mins (max 10 mins) in zone will kill you +sweeper.verbose = false +sweeper.flights = {} + +function sweeper.addSweeperZone(theZone) + sweeper.zones[theZone.name] = theZone +end + +function sweeper.readSweeperZone(theZone) + theZone.aircraft = theZone:getBoolFromZoneProperty("aircraft", true) + theZone.helos = theZone:getBoolFromZoneProperty("helos", false) +end + +function sweeper.update() + timer.scheduleFunction(sweeper.update, {}, timer.getTime() + sweeper.interval) + local toKill = {} + local newFlights = {} + for idx, theZone in pairs(sweeper.zones) do + for i= 0, 2 do + local allGroups = coalition.getGroups(i, 2) -- get all ground + for idy, theGroup in pairs(allGroups) do + local allUnits = theGroup:getUnits() + for idz, theUnit in pairs(allUnits) do + if theZone:unitInZone(theUnit) then + table.insert(toKill, theUnit) + end + end + end + if theZone.aircraft then + local allGroups = coalition.getGroups(i, 0) -- get all planes + for idy, theGroup in pairs(allGroups) do + local allUnits = theGroup:getUnits() + for idz, theUnit in pairs(allUnits) do + if theZone:unitInZone(theUnit) then + -- see if this was was already noted + uName = theUnit:getName() + if sweeper.flights[uName] then + table.insert(toKill, theUnit) + if sweeper.verbose then + trigger.action.outText("Sweeping aircraft <" .. uName .. "> off zone for obstruction", 30) + end + else + newFlights[uName] = true + if sweeper.verbose then + trigger.action.outText("sweep: aircraft <" .. uName .. "> on notice", 30) + end + end + end + end + end + end + + if theZone.helos then + local allGroups = coalition.getGroups(i, 1) -- get all helos + for idy, theGroup in pairs(allGroups) do + local allUnits = theGroup:getUnits() + for idz, theUnit in pairs(allUnits) do + if theZone:unitInZone(theUnit) then + -- see if this was was already noted + uName = theUnit:getName() + if sweeper.flights[uName] then + table.insert(toKill, theUnit) + if sweeper.verbose then + trigger.action.outText("Sweeping helicopter <" .. uName .. "> off zone for obstruction", 30) + end + else + newFlights[uName] = true + if sweeper.verbose then + trigger.action.outText("sweep: helicopter <" .. uName .. "> on notice", 30) + end + end + end + end + end + end + end + end + + -- remove all units in my kill list + for idx, theUnit in pairs(toKill) do + if theUnit.getPlayerName and theUnit:getPlayerName() then + -- we do not sweep players + else + if sweeper.verbose then + trigger.action.outText("*** sweeper: sweeping <" .. theUnit:getName() .. ">", 30) + end + if Unit.isExist(theUnit) then Unit.destroy(theUnit) end + end + end + + -- remember new list, forget old + sweeper.flights = newFlights +end + +function sweeper.start() + -- lib check + if not dcsCommon.libCheck then + trigger.action.outText("cfx sweeper requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx sweeper", sweeper.requiredLibs) then + return false + end + + -- process sweeper Zones + local attrZones = cfxZones.getZonesWithAttributeNamed("sweeper") + for k, aZone in pairs(attrZones) do + sweeper.readSweeperZone(aZone) -- process attributes + sweeper.addSweeperZone(aZone) -- add to list + end + + -- start update in 5 seconds + timer.scheduleFunction(sweeper.update, {}, timer.getTime() + sweeper.interval) + + -- say hi + trigger.action.outText("sweeper v" .. sweeper.version .. " started.", 30) + return true +end + +if not sweeper.start() then + trigger.action.outText("sweeper failed to start.", 30) + sweeper = nil +end \ No newline at end of file diff --git a/modules/tacan.lua b/modules/tacan.lua index aa3235a..eee0781 100644 --- a/modules/tacan.lua +++ b/modules/tacan.lua @@ -1,10 +1,13 @@ tacan = {} -tacan.version = "1.1.0" +tacan.version = "1.2.2" --[[-- Version History 1.0.0 - initial version 1.1.0 - OOP cfxZones - + 1.2.0 - desc attribute + 1.2.1 - actionSound config attribute + - sound with output + 1.2.2 - corrected typo when reading sound file for actionSound --]]-- tacan.verbose = false tacan.requiredLibs = { @@ -61,6 +64,10 @@ function tacan.createTacanZone(theZone) theZone.announcer = theZone:getBoolFromZoneProperty("announcer", false) + if theZone:hasProperty("desc") then + theZone.desc = theZone:getStringFromZoneProperty("desc", "") + end + -- interface to groupTracker if theZone:hasProperty("trackWith:") then theZone.trackWith = theZone:getStringFromZoneProperty( "trackWith:", "") @@ -138,6 +145,7 @@ function tacan.createTacanInZone(theZone, channel, mode, callsign) t.activeChan = channel t.theGroup = theGroup t.theData = theCopy + t.desc = theZone.desc table.insert(theZone.spawnedTACANS, t) -- run a GC cycle @@ -175,8 +183,10 @@ function tacan.TacanFromZone(theZone, silent) local str = "NOTAM: Deployed new TACAN " .. theZone.name .. " <" .. callsign .. ">, channel " .. channel .. mode .. ", active now" if theZone.coa == 0 then trigger.action.outText(str, 30) + trigger.action.outSound(tacan.actionSound) else trigger.action.outTextForCoalition(theZone.coa, str, 30) + trigger.action.outSoundForCoalition(theZone.coa, tacan.actionSound) end end end @@ -193,6 +203,7 @@ function tacan.destroyTacan(theZone, announce) trigger.action.outText(str, 30) else trigger.action.outTextForCoalition(coa, str, 30) + trigger.action.outSoundForCoalition(coa, tacan.actionSound) end end @@ -331,7 +342,6 @@ end function tacan.installComms() tacan.redC = missionCommands.addCommandForCoalition(1, "Available TACAN stations", nil, tacan.listTacan, 1) tacan.blueC = missionCommands.addCommandForCoalition(2, "Available TACAN stations", nil, tacan.listTacan, 2) - end function tacan.listTacan(side) @@ -353,6 +363,7 @@ function tacan.doListTacan(args) if #theTs < 1 then trigger.action.outTextForCoalition(args, "No active TACAN.", 30) + trigger.action.outSoundForCoalition(args, tacan.actionSound) return end @@ -360,9 +371,13 @@ function tacan.doListTacan(args) for idx, aTacan in pairs(theTs) do msg = msg .. "\n - " .. aTacan.activeCallsign .. ": " .. aTacan.activeChan .. aTacan.activeMode + if aTacan.desc then + msg = msg .. " - " .. aTacan.desc + end end msg = msg .. "\n" trigger.action.outTextForCoalition(args, msg, 30) + trigger.action.outSoundForCoalition(args, tacan.actionSound) end -- @@ -379,7 +394,7 @@ function tacan.readConfigZone() if theZone:hasProperty("GUI") then tacan.list = theZone:getBoolFromZoneProperty("GUI", false) end - + tacan.actionSound = theZone:getStringFromZoneProperty("actionSound", "Quest Snare 3.wav") if tacan.verbose then trigger.action.outText("+++tcn: read config", 30) end diff --git a/modules/wingTaskHelper.lua b/modules/wingTaskHelper.lua new file mode 100644 index 0000000..4d8e030 --- /dev/null +++ b/modules/wingTaskHelper.lua @@ -0,0 +1,632 @@ +wingTaskHelper = {} + +-- WP 1 Task for a CAS flight +wingTaskHelper.casTOTask = { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["enabled"] = true, + ["auto"] = true, + ["id"] = "EngageTargets", + ["number"] = 1, + ["key"] = "CAS", + ["params"] = + { + ["targetTypes"] = + { + [1] = "Helicopters", + [2] = "Ground Units", + [3] = "Light armed ships", + }, -- end of ["targetTypes"] + ["priority"] = 0, + }, -- end of ["params"] + }, -- end of [1] + [2] = + { + ["number"] = 2, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = 2, + ["name"] = 1, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [2] + [3] = + { + ["number"] = 3, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = 1, + ["name"] = 3, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [3] + [4] = + { + ["number"] = 4, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["variantIndex"] = 2, + ["name"] = 5, + ["formationIndex"] = 2, + ["value"] = 131074, -- trail formation + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [4] + [5] = + { + ["number"] = 5, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = true, + ["name"] = 15, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [5] + [6] = + { + ["number"] = 6, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["targetTypes"] = + { + }, -- end of ["targetTypes"] + ["name"] = 21, + ["value"] = "none;", + ["noTargetTypes"] = + { + [1] = "Fighters", + [2] = "Multirole fighters", + [3] = "Bombers", + [4] = "Helicopters", + [5] = "Infantry", + [6] = "Fortifications", + [7] = "Tanks", + [8] = "IFV", + [9] = "APC", + [10] = "Artillery", + [11] = "Unarmed vehicles", + [12] = "AAA", + [13] = "SR SAM", + [14] = "MR SAM", + [15] = "LR SAM", + [16] = "Aircraft Carriers", + [17] = "Cruisers", + [18] = "Destroyers", + [19] = "Frigates", + [20] = "Corvettes", + [21] = "Light armed ships", + [22] = "Unarmed ships", + [23] = "Submarines", + [24] = "Cruise missiles", + [25] = "Antiship Missiles", + [26] = "AA Missiles", + [27] = "AG Missiles", + [28] = "SA Missiles", + [29] = "UAVs", + }, -- end of ["noTargetTypes"] + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [6] + [7] = + { + ["number"] = 7, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = true, + ["name"] = 19, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [7] + [8] = + { + ["number"] = 8, + ["auto"] = false, + ["id"] = "EngageTargetsInZone", + ["enabled"] = true, + ["params"] = + { + ["targetTypes"] = + { + [1] = "All", + }, -- end of ["targetTypes"] + ["x"] = 19632.860434462, + ["y"] = 323453.87978006, + ["value"] = "All;", + ["noTargetTypes"] = + { + }, -- end of ["noTargetTypes"] + ["priority"] = 0, + ["zoneRadius"] = 76200, + }, -- end of ["params"] + }, -- end of [8] + }, -- end of ["tasks"] + }, -- end of ["params"] +} -- end of ["task"] + +wingTaskHelper.capTOTask = { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["number"] = 1, + ["key"] = "CAP", + ["id"] = "EngageTargets", + ["enabled"] = true, + ["auto"] = true, + ["params"] = + { + ["targetTypes"] = + { + [1] = "Air", + }, -- end of ["targetTypes"] + ["priority"] = 0, + }, -- end of ["params"] + }, -- end of [1] + [2] = + { + ["number"] = 2, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = true, + ["name"] = 17, -- restrict ground attack + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [2] + [3] = + { + ["number"] = 3, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = 0, + ["name"] = 18, -- max range launch + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [3] + [4] = + { + ["number"] = 4, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = true, + ["name"] = 19, -- no reporting + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [4] + [5] = + { + ["number"] = 5, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["targetTypes"] = + { + }, -- end of ["targetTypes"] + ["name"] = 21, + ["value"] = "none;", + ["noTargetTypes"] = + { + [1] = "Fighters", + [2] = "Multirole fighters", + [3] = "Bombers", + [4] = "Helicopters", + [5] = "Infantry", + [6] = "Fortifications", + [7] = "Tanks", + [8] = "IFV", + [9] = "APC", + [10] = "Artillery", + [11] = "Unarmed vehicles", + [12] = "AAA", + [13] = "SR SAM", + [14] = "MR SAM", + [15] = "LR SAM", + [16] = "Aircraft Carriers", + [17] = "Cruisers", + [18] = "Destroyers", + [19] = "Frigates", + [20] = "Corvettes", + [21] = "Light armed ships", + [22] = "Unarmed ships", + [23] = "Submarines", + [24] = "Cruise missiles", + [25] = "Antiship Missiles", + [26] = "AA Missiles", + [27] = "AG Missiles", + [28] = "SA Missiles", + [29] = "UAVs", + }, -- end of ["noTargetTypes"] + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [5] + [6] = + { + ["number"] = 6, + ["auto"] = false, + ["id"] = "EngageTargetsInZone", + ["enabled"] = true, + ["params"] = + { + ["targetTypes"] = + { + [1] = "Planes", + }, -- end of ["targetTypes"] + ["x"] = -1421.6419952991, + ["y"] = 311601.25461373, + ["value"] = "Planes;", + ["noTargetTypes"] = + { + }, -- end of ["noTargetTypes"] + ["priority"] = 0, + ["zoneRadius"] = 76200, + }, -- end of ["params"] + }, -- end of [6] + }, -- end of ["tasks"] + }, -- end of ["params"] +} -- end of ["task"] + +wingTaskHelper.seadTOTask = { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["number"] = 1, + ["key"] = "SEAD", + ["id"] = "EngageTargets", + ["enabled"] = true, + ["auto"] = true, + ["params"] = + { + ["targetTypes"] = + { + [1] = "Air Defence", + }, -- end of ["targetTypes"] + ["priority"] = 0, + }, -- end of ["params"] + }, -- end of [1] + [2] = + { + ["number"] = 2, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = 2, + ["name"] = 1, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [2] + [3] = + { + ["number"] = 3, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = 2, + ["name"] = 13, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [3] + [4] = + { + ["number"] = 4, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = true, + ["name"] = 19, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [4] + [5] = + { + ["number"] = 5, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["targetTypes"] = + { + [1] = "Air Defence", + }, -- end of ["targetTypes"] + ["name"] = 21, + ["value"] = "Air Defence;", + ["noTargetTypes"] = + { + [1] = "Fighters", + [2] = "Multirole fighters", + [3] = "Bombers", + [4] = "Helicopters", + [5] = "Infantry", + [6] = "Fortifications", + [7] = "Tanks", + [8] = "IFV", + [9] = "APC", + [10] = "Artillery", + [11] = "Unarmed vehicles", + [12] = "Aircraft Carriers", + [13] = "Cruisers", + [14] = "Destroyers", + [15] = "Frigates", + [16] = "Corvettes", + [17] = "Light armed ships", + [18] = "Unarmed ships", + [19] = "Submarines", + [20] = "Cruise missiles", + [21] = "Antiship Missiles", + [22] = "AA Missiles", + [23] = "AG Missiles", + [24] = "SA Missiles", + [25] = "UAVs", + }, -- end of ["noTargetTypes"] + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [5] + [6] = + { + ["number"] = 6, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "EPLRS", + ["params"] = + { + ["value"] = true, + ["groupId"] = 2, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [6] + [7] = + { + ["number"] = 7, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = true, + ["name"] = 15, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [7] + }, -- end of ["tasks"] + }, -- end of ["params"] +} -- end of ["task"] + +wingTaskHelper.bombTOTask = { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["number"] = 1, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = 2, + ["name"] = 1, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [1] + [2] = + { + ["number"] = 2, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "Option", + ["params"] = + { + ["value"] = true, + ["name"] = 15, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [2] + [3] = + { + ["number"] = 3, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "EPLRS", + ["params"] = + { + ["value"] = true, + ["groupId"] = 3, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [3] + }, -- end of ["tasks"] + }, -- end of ["params"] +} -- end of ["task"] + +wingTaskHelper.bombActionTask = { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["number"] = 1, + ["auto"] = false, + ["id"] = "CarpetBombing", + ["enabled"] = true, + ["params"] = + { + ["attackType"] = "Carpet", + ["attackQtyLimit"] = false, + ["attackQty"] = 1, + ["expend"] = "All", -- yay! + ["altitude"] = 7620, + ["x"] = -7222.3894291577, + ["carpetLength"] = 500, + ["y"] = 294267.9527197, + ["altitudeEnabled"] = false, + ["weaponType"] = 9663676414, + ["groupAttack"] = false, + }, -- end of ["params"] + }, -- end of [1] + }, -- end of ["tasks"] + }, -- end of ["params"] +} -- end of ["task"] \ No newline at end of file