diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 22f9892..1710560 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 e8b80d2..ecb650b 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/cfxReconMode.lua b/modules/cfxReconMode.lua index 992739b..82913e2 100644 --- a/modules/cfxReconMode.lua +++ b/modules/cfxReconMode.lua @@ -1,5 +1,5 @@ cfxReconMode = {} -cfxReconMode.version = "2.1.4" +cfxReconMode.version = "2.2.0" cfxReconMode.verbose = false -- set to true for debug info cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered @@ -86,6 +86,10 @@ VERSION HISTORY 2.1.3 - added cfxReconMode.name to allow direct acces with test zone flag 2.1.4 - canDetect() also checks if unit has been activated canDetect has strenghtened isExist() guard + 2.2.0 - new marksLocked config attribute, defaults to false + - new marksFadeAfter config attribute to control mark time + - dmlZones OOP upgrade + cfxReconMode is a script that allows units to perform reconnaissance missions and, after detecting units, marks them on the map with @@ -355,7 +359,7 @@ function cfxReconMode.placeMarkForUnit(location, theSide, theGroup) theDesc, location, theSide, - false, + cfxReconMode.marksLocked, -- readOnly -- false, nil) return theID end @@ -492,7 +496,7 @@ function cfxReconMode.processZoneMessage(inMsg, theZone, theGroup) -- replace with lat of zone point and with lon of zone point -- and with mgrs coords of zone point - local currPoint = cfxZones.getPoint(theZone) + local currPoint = theZone:getPoint() if theGroup and theGroup:isExist() then -- only use group's point when group exists and alive local theUnit = dcsCommon.getFirstLivingUnit(theGroup) @@ -529,7 +533,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) end -- put a mark on the map - if not silent and cfxReconMode.applyMarks then + if (not silent) and cfxReconMode.applyMarks then local theID = cfxReconMode.placeMarkForUnit(theLoc, mySide, theGroup) local gName = theGroup:getName() local args = {mySide, theScout, theGroup, theID, gName} @@ -541,10 +545,9 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) end -- say something - if not silent and cfxReconMode.announcer then + if (not silent) and cfxReconMode.announcer then local msg = cfxReconMode.generateSALT(theScout, theGroup) trigger.action.outTextForCoalition(mySide, msg, cfxReconMode.reportTime) --- trigger.action.outTextForCoalition(mySide, theScout:getName() .. " reports new ground contact " .. theGroup:getName(), 30) if cfxReconMode.verbose then trigger.action.outText("+++rcn: announced for side " .. mySide, 30) end @@ -554,7 +557,6 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) end -- see if it was a prio target - --local inList, gName = cfxReconMode.isStringInList(theGroup:getName(), cfxReconMode.prioList) if inList then -- if cfxReconMode.announcer then if cfxReconMode.verbose then @@ -565,7 +567,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) -- increase prio flag if cfxReconMode.prioFlag then - cfxZones.pollFlag(cfxReconMode.prioFlag, cfxReconMode.method, cfxReconMode.theZone) + cfxReconMode.theZone:pollFlag(cfxReconMode.prioFlag, cfxReconMode.method ) end -- see if we were passed additional info in zInfo @@ -583,7 +585,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) end if zInfo.theFlag then - cfxZones.pollFlag(zInfo.theFlag, cfxReconMode.method, zInfo.theZone) + zInfo.theZone:pollFlag(zInfo.theFlag, cfxReconMode.method) if cfxReconMode.verbose or zInfo.theZone.verbose then trigger.action.outText("+++rcn: banging <" .. zInfo.theFlag .. "> for prio target zone <" .. zInfo.theZone.name .. ">",30) end @@ -595,7 +597,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) -- increase normal flag if cfxReconMode.detectFlag then - cfxZones.pollFlag(cfxReconMode.detectFlag, cfxReconMode.method, cfxReconMode.theZone) + cfxReconMode.theZone:pollFlag(cfxReconMode.detectFlag, cfxReconMode.method) end end end @@ -655,7 +657,7 @@ function cfxReconMode.doDeActivate() end end -function cfxReconMode.updateQueues() +function cfxReconSMode.updateQueues() -- schedule next call timer.scheduleFunction(cfxReconMode.updateQueues, {}, timer.getTime() + 1/cfxReconMode.ups) @@ -681,7 +683,6 @@ function cfxReconMode.updateQueues() -- the scouts array, move it to processed and then shrink -- scouts table until it's empty. When empty, transfer all -- back and start cycle anew - local theFocusScoutName = nil local procCount = 0 -- no iterations done yet for name, scout in pairs(cfxReconMode.scouts) do @@ -736,7 +737,6 @@ function cfxReconMode.autoRemove() local toRemove = {} -- scan all marked groups, and when they no longer exist, remove them for idx, args in pairs (cfxReconMode.activeMarks) do - -- args = {mySide, theScout, theGroup, theID, gName} local gName = args[5] if not cfxReconMode.isGroupStillAlive(gName) then -- remove mark, remove group from set @@ -766,7 +766,7 @@ function cfxReconMode.lateEvalPlayerUnit(theUnit) for idx, theZone in pairs (cfxReconMode.scoutZones) do local isScout = theZone.isScout local dynamic = theZone.dynamic - local inZone = cfxZones.pointInZone(p, theZone) + local inZone = theZone:pointInZone(p) if inZone then if isScout then cfxReconMode.addToAllowedScoutList(aGroup, dynamic) @@ -886,7 +886,6 @@ function cfxReconMode:onEvent(event) end cfxReconMode.addScout(theUnit) end --- trigger.action.outText("+++rcn-onEvent: " .. event.id .. " for <" .. theUnit:getName() .. ">", 30) end -- @@ -984,68 +983,70 @@ function cfxReconMode.readConfigZone() end end - cfxReconMode.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + cfxReconMode.verbose = theZone.verbose --cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) - cfxReconMode.autoRecon = cfxZones.getBoolFromZoneProperty(theZone, "autoRecon", true) - cfxReconMode.redScouts = cfxZones.getBoolFromZoneProperty(theZone, "redScouts", false) - cfxReconMode.blueScouts = cfxZones.getBoolFromZoneProperty(theZone, "blueScouts", true) - cfxReconMode.greyScouts = cfxZones.getBoolFromZoneProperty(theZone, "greyScouts", false) - cfxReconMode.playerOnlyRecon = cfxZones.getBoolFromZoneProperty(theZone, "playerOnlyRecon", false) - cfxReconMode.reportNumbers = cfxZones.getBoolFromZoneProperty(theZone, "reportNumbers", true) - cfxReconMode.reportTime = cfxZones.getNumberFromZoneProperty(theZone, "reportTime", 30) + cfxReconMode.autoRecon = theZone:getBoolFromZoneProperty("autoRecon", true) + cfxReconMode.redScouts = theZone:getBoolFromZoneProperty("redScouts", false) + cfxReconMode.blueScouts = theZone:getBoolFromZoneProperty( "blueScouts", true) + cfxReconMode.greyScouts = theZone:getBoolFromZoneProperty( "greyScouts", false) + cfxReconMode.playerOnlyRecon = theZone:getBoolFromZoneProperty("playerOnlyRecon", false) + cfxReconMode.reportNumbers = theZone:getBoolFromZoneProperty( "reportNumbers", true) + cfxReconMode.reportTime = theZone:getNumberFromZoneProperty( "reportTime", 30) - cfxReconMode.detectionMinRange = cfxZones.getNumberFromZoneProperty(theZone, "detectionMinRange", 3000) - cfxReconMode.detectionMaxRange = cfxZones.getNumberFromZoneProperty(theZone, "detectionMaxRange", 12000) - cfxReconMode.maxAlt = cfxZones.getNumberFromZoneProperty(theZone, "maxAlt", 9000) + cfxReconMode.detectionMinRange = theZone:getNumberFromZoneProperty("detectionMinRange", 3000) + cfxReconMode.detectionMaxRange = theZone:getNumberFromZoneProperty("detectionMaxRange", 12000) + cfxReconMode.maxAlt = theZone:getNumberFromZoneProperty("maxAlt", 9000) - if cfxZones.hasProperty(theZone, "prio+") then - cfxReconMode.prioFlag = cfxZones.getStringFromZoneProperty(theZone, "prio+", "none") - elseif cfxZones.hasProperty(theZone, "prio!") then - cfxReconMode.prioFlag = cfxZones.getStringFromZoneProperty(theZone, "prio!", "*") + if theZone:hasProperty("prio+") then -- deprecated. remove next update + cfxReconMode.prioFlag = theZone:getStringFromZoneProperty("prio+", "none") + elseif theZone:hasProperty("prio!") then + cfxReconMode.prioFlag = theZone:getStringFromZoneProperty("prio!", "*") end - if cfxZones.hasProperty(theZone, "detect+") then - cfxReconMode.detectFlag = cfxZones.getStringFromZoneProperty(theZone, "detect+", "none") - elseif cfxZones.hasProperty(theZone, "detect!") then - cfxReconMode.detectFlag = cfxZones.getStringFromZoneProperty(theZone, "detect!", "*") + if theZone:hasProperty("detect+") then -- deprecated + cfxReconMode.detectFlag = theZone:getStringFromZoneProperty("detect+", "none") + elseif theZone:hasProperty("detect!") then + cfxReconMode.detectFlag = theZone:getStringFromZoneProperty("detect!", "*") end - cfxReconMode.method = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") - if cfxZones.hasProperty(theZone, "reconMethod") then - cfxReconMode.method = cfxZones.getStringFromZoneProperty(theZone, "reconMethod", "inc") + cfxReconMode.method = theZone:getStringFromZoneProperty("method", "inc") + if theZone:hasProperty("reconMethod") then + cfxReconMode.method = theZone:getStringFromZoneProperty("reconMethod", "inc") end - cfxReconMode.applyMarks = cfxZones.getBoolFromZoneProperty(theZone, "applyMarks", true) - cfxReconMode.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true) - -- trigger.action.outText("recon: announcer is " .. dcsCommon.bool2Text(cfxReconMode.announcer), 30) -- announced - if cfxZones.hasProperty(theZone, "reconSound") then - cfxReconMode.reconSound = cfxZones.getStringFromZoneProperty(theZone, "reconSound", "") + cfxReconMode.applyMarks = theZone:getBoolFromZoneProperty( "applyMarks", true) + cfxReconMode.marksFadeAfter = theZone:getNumberFromZoneProperty("marksFadeAfter", 30*60) -- 30 minutes default + cfxReconMode.marksLocked = theZone:getBoolFromZoneProperty("marksLocked", false) -- if true, players cannot remove the marks + cfxReconMode.announcer = theZone:getBoolFromZoneProperty( "announcer", true) + + if theZone:hasProperty("reconSound") then + cfxReconMode.reconSound = theZone:getStringFromZoneProperty("reconSound", "") end - cfxReconMode.removeWhenDestroyed = cfxZones.getBoolFromZoneProperty(theZone, "autoRemove", true) + cfxReconMode.removeWhenDestroyed = theZone:getBoolFromZoneProperty("autoRemove", true) - cfxReconMode.mgrs = cfxZones.getBoolFromZoneProperty(theZone, "mgrs", false) + cfxReconMode.mgrs = theZone:getBoolFromZoneProperty("mgrs", false) - cfxReconMode.active = cfxZones.getBoolFromZoneProperty(theZone, "active", true) - if cfxZones.hasProperty(theZone, "activate?") then - cfxReconMode.activate = cfxZones.getStringFromZoneProperty(theZone, "activate?", "*") - cfxReconMode.lastActivate = cfxZones.getFlagValue(cfxReconMode.activate, theZone) - elseif cfxZones.hasProperty(theZone, "on?") then - cfxReconMode.activate = cfxZones.getStringFromZoneProperty(theZone, "on?", "*") - cfxReconMode.lastActivate = cfxZones.getFlagValue(cfxReconMode.activate, theZone) + cfxReconMode.active = theZone:getBoolFromZoneProperty("active", true) + if theZone:hasProperty("activate?") then + cfxReconMode.activate = theZone:getStringFromZoneProperty("activate?", "*") + cfxReconMode.lastActivate = theZone:getFlagValue(cfxReconMode.activate) + elseif theZone:hasProperty("on?") then + cfxReconMode.activate = theZone:getStringFromZoneProperty("on?", "*") + cfxReconMode.lastActivate = theZone:getFlagValue(cfxReconMode.activate) end - if cfxZones.hasProperty(theZone, "deactivate?") then - cfxReconMode.deactivate = cfxZones.getStringFromZoneProperty(theZone, "deactivate?", "*") - cfxReconMode.lastDeActivate = cfxZones.getFlagValue(cfxReconMode.deactivate, theZone) - elseif cfxZones.hasProperty(theZone, "off?") then - cfxReconMode.deactivate = cfxZones.getStringFromZoneProperty(theZone, "off?", "*") - cfxReconMode.lastDeActivate = cfxZones.getFlagValue(cfxReconMode.deactivate, theZone) + if theZone:hasProperty("deactivate?") then + cfxReconMode.deactivate = theZone:getStringFromZoneProperty("deactivate?", "*") + cfxReconMode.lastDeActivate = theZone:getFlagValue(cfxReconMode.deactivate) + elseif theZone:hasProperty("off?") then + cfxReconMode.deactivate = theZone:getStringFromZoneProperty("off?", "*") + cfxReconMode.lastDeActivate = theZone:getFlagValue(cfxReconMode.deactivate) end - cfxReconMode.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperial", false) - if cfxZones.hasProperty(theZone, "imperialUnits") then - cfxReconMode.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperialUnits", false) + cfxReconMode.imperialUnits = theZone:getBoolFromZoneProperty("imperial", false) + if theZone:hasProperty("imperialUnits") then + cfxReconMode.imperialUnits = theZone:getBoolFromZoneProperty( "imperialUnits", false) end cfxReconMode.theZone = theZone -- save this zone @@ -1057,24 +1058,24 @@ end function cfxReconMode.processReconZone(theZone) - local theList = cfxZones.getStringFromZoneProperty(theZone, "recon", "prio") + local theList = theZone:getStringFromZoneProperty("recon", "prio") theList = string.upper(theList) local isBlack = dcsCommon.stringStartsWith(theList, "BLACK") local zInfo = {} zInfo.theZone = theZone zInfo.isBlack = isBlack - zInfo.silent = cfxZones.getBoolFromZoneProperty(theZone, "silent", false) + zInfo.silent = theZone:getBoolFromZoneProperty("silent", false) - if cfxZones.hasProperty(theZone, "spotted!") then - zInfo.theFlag = cfxZones.getStringFromZoneProperty(theZone, "spotted!", "*") + if theZone:hasProperty("spotted!") then + zInfo.theFlag = theZone:getStringFromZoneProperty("spotted!", "*") end - if cfxZones.hasProperty(theZone, "prioMessage") then - zInfo.prioMessage = cfxZones.getStringFromZoneProperty(theZone, "prioMessage", "") + if theZone:hasProperty("prioMessage") then + zInfo.prioMessage = theZone:getStringFromZoneProperty("prioMessage", "") end - local dynamic = cfxZones.getBoolFromZoneProperty(theZone, "dynamic", false) + local dynamic = theZone:getBoolFromZoneProperty("dynamic", false) zInfo.dynamic = dynamic local categ = 2 -- ground troops only local allGroups = cfxZones.allGroupsInZone(theZone, categ) @@ -1100,15 +1101,15 @@ function cfxReconMode.processReconZone(theZone) end function cfxReconMode.processScoutZone(theZone) - local isScout = cfxZones.getBoolFromZoneProperty(theZone, "scout", true) - local dynamic = cfxZones.getBoolFromZoneProperty(theZone, "dynamic") + local isScout = theZone:getBoolFromZoneProperty("scout", true) + local dynamic = theZone:getBoolFromZoneProperty("dynamic") theZone.dynamic = dynamic theZone.isScout = isScout local categ = 0 -- aircraft - local allFixed = cfxZones.allGroupsInZone(theZone, categ) + local allFixed = theZone:allGroupsInZone(categ) local categ = 1 -- helos - local allRotor = cfxZones.allGroupsInZone(theZone, categ) + local allRotor = theZone:allGroupsInZone(categ) local allGroups = dcsCommon.combineTables(allFixed, allRotor) for idx, aGroup in pairs(allGroups) do if isScout then diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 7619c46..6dde883 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -149,6 +149,7 @@ cfxZones.version = "4.0.0" - getNumberFromZoneProperty() enforces number return even on default - immediate method switched to preceeding '#', to resolve conflict witzh negative numbers, backwards compatibility with old (dysfunctional) method +- 4.0.1 - dmlZone:getName() --]]-- -- @@ -3258,6 +3259,10 @@ function dmlZone:getPoint(getHeight) return thePos end +function dmlZone:getName() -- no cfxZones.bridge! + return self.name +end + function cfxZones.linkUnitToZone(theUnit, theZone, dx, dy) -- note: dy is really Z, don't get confused!!!! theZone.linkedUnit = theUnit if not dx then dx = 0 end diff --git a/modules/civAir.lua b/modules/civAir.lua index 6c2bf4b..0d4b740 100644 --- a/modules/civAir.lua +++ b/modules/civAir.lua @@ -1,5 +1,5 @@ civAir = {} -civAir.version = "1.5.2" +civAir.version = "2.0.0" --[[-- 1.0.0 initial version 1.1.0 exclude list for airfields @@ -23,7 +23,15 @@ civAir.version = "1.5.2" exclude list and include list 1.5.1 added depart only and arrive only options for airfields 1.5.2 fixed bugs inb verbosity - + 2.0.0 dmlZones + inbound zones + outbound zones + on start location is randomizes 30-70% of the way + guarded 'no longer exist' warning for verbosity + changed unit naming from -civA to -GA + strenghtened guard on testing against free slots for other units + flights are now of random neutral countries + maxFlights synonym for maxTraffic --]]-- @@ -48,22 +56,22 @@ civAir.trafficCenters = {} -- If the attribute's value is anything -- but "exclude", the closest airfield to the zone -- is added to trafficCenters - -- if you leave this list empty, and do not add airfields -- by zones, the list is automatically populated with all -- airfields in the map +-- if name starts with "***" then it is not an airfield, but zone civAir.excludeAirfields = {} -- list all airfields that must NOT be included in -- civilian activities. Will be used for neither landing -- nor departure. overrides any airfield that was included --- in trafficCenters. Here, Senaki is off limits for --- civilian air traffic +-- in trafficCenters. -- can be populated by zone on the map that have the -- 'civAir' attribute with value "exclude" civAir.departOnly = {} -- use only to start from civAir.landingOnly = {} -- use only to land at +civAir.inoutZones = {} -- off-map connector zones civAir.requiredLibs = { "dcsCommon", -- common is of course needed for everything @@ -72,57 +80,73 @@ civAir.requiredLibs = { civAir.activePlanes = {} civAir.idlePlanes = {} +civAir.outboundFlights = {} -- only flights that are enroute to an outbound zone function civAir.readConfigZone() -- note: must match exactly!!!! local theZone = cfxZones.getZoneByName("civAirConfig") if not theZone then trigger.action.outText("***civA: NO config zone!", 30) - return + theZone = cfxZones.createSimpleZone("civAirConfig") end - - trigger.action.outText("civA: found config zone!", 30) - + -- ok, for each property, load it if it exists - if cfxZones.hasProperty(theZone, "aircraftTypes") then - local theTypes = cfxZones.getStringFromZoneProperty(theZone, "aircraftTypes", "Yak-40") + if theZone:hasProperty("aircraftTypes") then + local theTypes = theZone:getStringFromZoneProperty( "aircraftTypes", civAir.aircraftTypes) -- "Yak-40") local typeArray = dcsCommon.splitString(theTypes, ",") typeArray = dcsCommon.trimArray(typeArray) civAir.aircraftTypes = typeArray end - if cfxZones.hasProperty(theZone, "ups") then - civAir.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 0.05) + if theZone:hasProperty("ups") then + civAir.ups = theZone:getNumberFromZoneProperty("ups", 0.05) if civAir.ups < .0001 then civAir.ups = 0.05 end end - if cfxZones.hasProperty(theZone, "maxTraffic") then - civAir.maxTraffic = cfxZones.getNumberFromZoneProperty(theZone, "maxTraffic", 10) + if theZone:hasProperty("maxTraffic") then + civAir.maxTraffic = theZone:getNumberFromZoneProperty( "maxTraffic", 10) + elseif theZone:hasProperty("maxFlights") then + civAir.maxTraffic = theZone:getNumberFromZoneProperty( "maxFlights", 10) end - if cfxZones.hasProperty(theZone, "maxIdle") then - civAir.maxIdle = cfxZones.getNumberFromZoneProperty(theZone, "maxIdle", 8 * 60) + if theZone:hasProperty("maxIdle") then + civAir.maxIdle = theZone:getNumberFromZoneProperty("maxIdle", 8 * 60) end - if cfxZones.hasProperty(theZone, "initialAirSpawns") then - civAir.initialAirSpawns = cfxZones.getBoolFromZoneProperty(theZone, "initialAirSpawns", true) + if theZone:hasProperty("initialAirSpawns") then + civAir.initialAirSpawns = theZone:getBoolFromZoneProperty( "initialAirSpawns", true) end - civAir.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + civAir.verbose = theZone.verbose -- cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) end function civAir.processZone(theZone) - local value = cfxZones.getStringFromZoneProperty(theZone, "civAir", "") + local value = theZone:getStringFromZoneProperty("civAir", "") local af = dcsCommon.getClosestAirbaseTo(theZone.point, 0) -- 0 = only airfields, not farp or ships + local inoutName = "***" .. theZone:getName() + if af then local afName = af:getName() value = value:lower() - if value == "exclude" then + if value == "exclude" or value == "closed" then table.insert(civAir.excludeAirfields, afName) - elseif dcsCommon.stringStartsWith(value, "depart") or dcsCommon.stringStartsWith(value, "start") then + elseif dcsCommon.stringStartsWith(value, "depart") or dcsCommon.stringStartsWith(value, "start") or dcsCommon.stringStartsWith(value, "take") then table.insert(civAir.departOnly, afName) elseif dcsCommon.stringStartsWith(value, "land") or dcsCommon.stringStartsWith(value, "arriv") then table.insert(civAir.landingOnly, afName) + elseif dcsCommon.stringStartsWith(value, "inb") then + table.insert(civAir.departOnly, inoutName) -- start in inbound zone + civAir.inoutZones[inoutName] = theZone +-- theZone.inbound = true + elseif dcsCommon.stringStartsWith(value, "outb") then + table.insert(civAir.landingOnly, inoutName) + civAir.inoutZones[inoutName] = theZone +-- theZone.outbound = true + elseif dcsCommon.stringStartsWith(value, "in/out") then + table.insert(civAir.trafficCenters, inoutName) + civAir.inoutZones[inoutName] = theZone +-- theZone.inbound = true +-- theZone.outbound = true else table.insert(civAir.trafficCenters, afName) -- note that adding the same twice makes it more likely to be picked end @@ -142,10 +166,11 @@ function civAir.removePlaneGroupByName(aName) return end if civAir.activePlanes[aName] then - --trigger.action.outText("civA: REMOVING " .. aName .. " ***", 30) civAir.activePlanes[aName] = nil else - trigger.action.outText("civA: warning - ".. aName .." remove req but not found", 30) + if civAir.verbose then + trigger.action.outText("civA: warning - ".. aName .." remove req but not found", 30) + end end end @@ -207,10 +232,25 @@ function civAir.getTwoAirbases() tries = tries + 1 -- only try 10 times until fAB ~= sAB or tries > 10 - fAB = dcsCommon.getFirstAirbaseWhoseNameContains(fAB, 0) - sAB = dcsCommon.getFirstAirbaseWhoseNameContains(sAB, 0) + + local civA = {} + if not (dcsCommon.stringStartsWith(fAB, '***')) then + civA.AB = dcsCommon.getFirstAirbaseWhoseNameContains(fAB, 0) + civA.name = civA.AB:getName() + else + civA.zone = civAir.inoutZones[fAB] + civA.name = civA.zone:getName() + end + local civB = {} + if not (dcsCommon.stringStartsWith(sAB, '***')) then + civB.AB = dcsCommon.getFirstAirbaseWhoseNameContains(sAB, 0) + civB.name = civB.AB:getName() + else + civB.zone = civAir.inoutZones[sAB] + civB.name = civB.zone:getName() + end - return fAB, sAB + return civA, civB -- fAB, sAB end function civAir.parkingIsFree(fromWP) @@ -222,9 +262,9 @@ function civAir.parkingIsFree(fromWP) loc.z = fromWP.z for name, aPlaneGroup in pairs(civAir.activePlanes) do - if aPlaneGroup:isExist() then + if Group.isExist(aPlaneGroup) then local aPlane = aPlaneGroup:getUnit(1) - if aPlane:isExist() then + if aPlane and Unit.isExist(aPlane) then pos = aPlane:getPoint() local delta = dcsCommon.dist(loc, pos) if delta < 21 then @@ -242,21 +282,35 @@ end civAir.airStartSeparation = 0 function civAir.createFlight(name, theTypeString, fromAirfield, toAirfield, inAirStart) if not fromAirfield then - trigger.action.outText("civA: NIL fromAirfield", 30) + trigger.action.outText("civA: NIL source", 30) return nil end if not toAirfield then - trigger.action.outText("civA: NIL toAirfield", 30) + trigger.action.outText("civA: NIL destination", 30) return nil end + local randomizeLoc = inAirStart + local theGroup = dcsCommon.createEmptyAircraftGroupData (name) - local theAUnit = dcsCommon.createAircraftUnitData(name .. "-civA", theTypeString, false) + local theAUnit = dcsCommon.createAircraftUnitData(name .. "-GA", theTypeString, false) theAUnit.payload.fuel = 100000 dcsCommon.addUnitToGroupData(theAUnit, theGroup) - local fromWP = dcsCommon.createTakeOffFromParkingRoutePointData(fromAirfield) + local fromWP + if fromAirfield.AB then + fromWP = dcsCommon.createTakeOffFromParkingRoutePointData(fromAirfield.AB) + else + -- we start in air from inside inbound zone + local p = fromAirfield.zone:createRandomPointInZone() + local alt = fromAirfield.zone:getNumberFromZoneProperty("alt", 8000) + fromWP = dcsCommon.createSimpleRoutePointData(p, alt) + theAUnit.alt = fromWP.alt + theAUnit.speed = fromWP.speed + inAirStart = false -- it already is, no separation shenigans + end + if not fromWP then trigger.action.outText("civA: fromWP create failed", 30) return nil @@ -266,38 +320,81 @@ function civAir.createFlight(name, theTypeString, fromAirfield, toAirfield, inAi fromWP.alt = fromWP.alt + 3000 + civAir.airStartSeparation -- 9000 ft overhead + separation fromWP.action = "Turning Point" fromWP.type = "Turning Point" - + fromWP.speed = 150; fromWP.airdromeId = nil theAUnit.alt = fromWP.alt theAUnit.speed = fromWP.speed end - -- sometimes, when landing kicks in too early, the plane lands - -- at the wrong airfield. AI sucks. - -- so we force overflight of target airfield - local overheadWP = dcsCommon.createOverheadAirdromeRoutPintData(toAirfield) - local toWP = dcsCommon.createLandAtAerodromeRoutePointData(toAirfield) - if not toWP then - trigger.action.outText("civA: toWP create failed", 30) - return nil - end - if not civAir.parkingIsFree(fromWP) then - trigger.action.outText("civA: failed free parking check for flight " .. name, 30) - return nil + -- now look at destination: airfield or zone? + local zoneApproach = toAirfield.zone + local toWP + local overheadWP + if zoneApproach then + -- we fly this plane to a zone, and then disappear it + local p = zoneApproach:getPoint() + local alt = zoneApproach:getNumberFromZoneProperty("alt", 8000) + toWP = dcsCommon.createSimpleRoutePointData(p, alt) + else + -- sometimes, when landing kicks in too early, the plane lands + -- at the wrong airfield. AI sucks. + -- so we force overflight of target airfield + overheadWP = dcsCommon.createOverheadAirdromeRoutPintData(toAirfield.AB) + toWP = dcsCommon.createLandAtAerodromeRoutePointData(toAirfield.AB) + if not toWP then + trigger.action.outText("civA: toWP create failed", 30) + return nil + end + + if not civAir.parkingIsFree(fromWP) then + trigger.action.outText("civA: failed free parking check for flight " .. name, 30) + return nil + end + end + + if randomizeLoc then + -- make first wp to somewhere 30-70 towards toWP + local percent = (math.random(40) + 30) / 100 + local mx = dcsCommon.lerp(fromWP.x, toWP.x, percent) + local my = dcsCommon.lerp(fromWP.y, toWP.y, percent) + fromWP.x = mx + fromWP.y = my + fromWP.speed = 150 + fromWP.alt = 8000 + theAUnit.alt = fromWP.alt + theAUnit.speed = fromWP.speed + end + + if (not fromAirfield.AB) or randomizedLoc or inAirStart then + -- set current heading correct towards toWP + local hdg = dcsCommon.bearingFromAtoBusingXY(fromWP, toWP) + theAUnit.heading = hdg + theAUnit.psi = -hdg end dcsCommon.moveGroupDataTo(theGroup, fromWP.x, fromWP.y) dcsCommon.addRoutePointForGroupData(theGroup, fromWP) - dcsCommon.addRoutePointForGroupData(theGroup, overheadWP) + if not zoneApproach then + dcsCommon.addRoutePointForGroupData(theGroup, overheadWP) + end dcsCommon.addRoutePointForGroupData(theGroup, toWP) -- spawn local groupCat = Group.Category.AIRPLANE - local theSpawnedGroup = coalition.addGroup(82, groupCat, theGroup) -- 82 is UN peacekeepers + local allNeutral = dcsCommon.getCountriesForCoalition(0) + local aRandomNeutral = dcsCommon.pickRandom(allNeutral) + if not aRandomNeutral then + trigger.action.outText("+++civA: WARNING: no neutral countries exist, flight is not neutral.", 30) + end + local theSpawnedGroup = coalition.addGroup(aRandomNeutral, groupCat, theGroup) -- 82 is UN peacekeepers + if zoneApproach then + -- track this flight to target zone + civAir.outboundFlights[name] = zoneApproach + end return theSpawnedGroup end @@ -312,7 +409,9 @@ function civAir.createNewFlight(inAirStart) return end - local name = fAB:getName() .. "-" .. sAB:getName().. "/" .. civAir.flightCount + -- fAB and sAB are tables that have either .base or AB set + + local name = fAB.name .. "-" .. sAB.name.. "/" .. civAir.flightCount local TypeString = dcsCommon.pickRandom(civAir.aircraftTypes) local theFlight = civAir.createFlight(name, TypeString, fAB, sAB, inAirStart) @@ -325,7 +424,7 @@ function civAir.createNewFlight(inAirStart) civAir.addPlane(theFlight) -- track it if civAir.verbose then - trigger.action.outText("civA: created flight from <" .. fAB:getName() .. "> to <" .. sAB:getName() .. ">", 30) + trigger.action.outText("civA: created flight from <" .. fAB.name .. "> to <" .. sAB.name .. ">", 30) end end @@ -340,9 +439,40 @@ function civAir.airStartPopulation() end -- --- U P D A T E L O O P +-- U P D A T E L O O P S -- +function civAir.trackOutbound() + timer.scheduleFunction(civAir.trackOutbound, {}, timer.getTime() + 10) + + -- iterate all flights that are outbound + local filtered = {} + for gName, theZone in pairs(civAir.outboundFlights) do + local theGroup = Group.getByName(gName) + if theGroup then + local theUnit = theGroup:getUnit(1) + if theUnit and Unit.isExist(theUnit) then + local p = theUnit:getPoint() + local t = theZone:getPoint() + local d = dcsCommon.distFlat(p, t) + if d > 3000 then -- works unless plane faster than 300m/s = 1080 km/h + -- keep watching + filtered[gName] = theZone + else + -- we can disappear the group + if civAir.verbose then + trigger.action.outText("+++civA: flight <" .. gName .. "> has reached map outbound zone <" .. theZone:getName() .. "> and is removed", 30) + end + Group.destroy(theGroup) + end + else + trigger.action.outText("+++civ: lost unit in group <" .. gName .. "> heading for <" .. theZone:getName() .. ">", 30) + end + end + end + civAir.outboundFlights = filtered +end + function civAir.update() -- reschedule me in the future. ups = updates per second. timer.scheduleFunction(civAir.update, {}, timer.getTime() + 1/civAir.ups) @@ -359,12 +489,15 @@ function civAir.update() for idx, name in pairs(removeMe) do civAir.activePlanes[name] = nil - trigger.action.outText("civA: warning - removed " .. name .. " from active roster, no longer exists", 30) + if civAir.verbose then + trigger.action.outText("civA: removed " .. name .. " from active roster, no longer exists", 30) + end end -- now, run through all existing flights and update their -- idle times. also count how many planes there are + -- so we can respawn if we are below max local planeNum = 0 local overduePlanes = {} local now = timer.getTime() @@ -372,18 +505,17 @@ function civAir.update() local speed = 0 if aPlaneGroup:isExist() then local aPlane = aPlaneGroup:getUnit(1) - - if aPlane and aPlane:isExist() and aPlane:getLife() >= 1 then + if aPlane and Unit.isExist(aPlane) and aPlane:getLife() >= 1 then planeNum = planeNum + 1 local vel = aPlane:getVelocity() speed = dcsCommon.mag(vel.x, vel.y, vel.z) else - -- force removal of group + -- force removal of group, plane no longer exists civAir.idlePlanes[name] = -1000 speed = 0 end else - -- force removal + -- force removal, group no longer exists civAir.idlePlanes[name] = -1000 speed = 0 end @@ -398,10 +530,9 @@ function civAir.update() table.insert(overduePlanes, name) end else - -- zero out idle plane + -- zero out idle plane, it's moving fast enough civAir.idlePlanes[name] = nil end - --]]-- end -- see if we have less than max flights running @@ -414,9 +545,12 @@ function civAir.update() for idx, aName in pairs(overduePlanes) do local aFlight = civAir.getPlane(aName) -- returns a group civAir.removePlaneGroupByName(aName) -- remove from roster - if aFlight and aFlight:isExist() then + if aFlight and Unit.isExist(aFlight) then -- destroy can only work if group isexist! Group.destroy(aFlight) -- remember: flights are groups! + if civAir.verbose then + trigger.action.outText("+++civA: removed flight <" .. aName .. "> for overtime.", 30) + end end end end @@ -504,7 +638,9 @@ function civAir.start() -- start the update loop civAir.update() - + -- start outbound tracking + civAir.trackOutbound() + -- say hi! trigger.action.outText("cf/x civAir v" .. civAir.version .. " started.", 30) return true @@ -518,10 +654,11 @@ end --[[-- Additional ideas - - border zones: ac can airstart in there and disappear in there - callbacks for civ spawn / despawn - add civkill callback / redCivKill blueCivKill flag bangers - Helicopter support - - departure only, destination only - add slot checking to see if other planes block it even though DCS claims the slot is free + - allow list of countries to choose civ air from + - ability to force a flight from a source? How do we make a destination? currently not a good idea + --]]-- \ No newline at end of file diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index a79b33b..b6344c6 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.9.0" +dcsCommon.version = "2.9.2" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -166,7 +166,12 @@ dcsCommon.version = "2.9.0" 2.9.0 - createPoint() moved from cfxZones - copyPoint() moved from cfxZones - numberArrayFromString() moved from cfxZones - +2.9.1 - new createSimpleRoutePointData() + - createOverheadAirdromeRoutPintData corrected and legacy support added + - new bearingFromAtoBusingXY() + - corrected verbosity for bearingFromAtoB + - new getCountriesForCoalition() +2.9.2 - updated task2text --]]-- @@ -182,6 +187,7 @@ dcsCommon.version = "2.9.0" dcsCommon.cbID = 0 -- callback id for simple callback scheduling dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P"} -- Ka-50, Apache and Gazelle can't carry troops dcsCommon.coalitionSides = {0, 1, 2} + dcsCommon.maxCountry = 86 -- number of countries defined in total -- lookup tables dcsCommon.groupID2Name = {} @@ -747,23 +753,23 @@ dcsCommon.version = "2.9.0" return 0 end if not B then - trigger.action.outText("WARNING: no 'A' in bearingFromAtoB", 30) + trigger.action.outText("WARNING: no 'B' in bearingFromAtoB", 30) return 0 end if not A.x then trigger.action.outText("WARNING: no 'A.x' (type A =<" .. type(A) .. ">)in bearingFromAtoB", 30) return 0 end - if not A.y then - trigger.action.outText("WARNING: no 'A.x' (type A =<" .. type(A) .. ">)in bearingFromAtoB", 30) + if not A.z then + trigger.action.outText("WARNING: no 'A.z' (type A =<" .. type(A) .. ">)in bearingFromAtoB", 30) return 0 end if not B.x then trigger.action.outText("WARNING: no 'B.x' (type B =<" .. type(B) .. ">)in bearingFromAtoB", 30) return 0 end - if not B.y then - trigger.action.outText("WARNING: no 'B.y' (type B =<" .. type(B) .. ">)in bearingFromAtoB", 30) + if not B.z then + trigger.action.outText("WARNING: no 'B.z' (type B =<" .. type(B) .. ">)in bearingFromAtoB", 30) return 0 end @@ -773,6 +779,38 @@ dcsCommon.version = "2.9.0" return bearing end + function dcsCommon.bearingFromAtoBusingXY(A, B) -- coords in x, y + if not A then + trigger.action.outText("WARNING: no 'A' in bearingFromAtoBXY", 30) + return 0 + end + if not B then + trigger.action.outText("WARNING: no 'B' in bearingFromAtoBXY", 30) + return 0 + end + if not A.x then + trigger.action.outText("WARNING: no 'A.x' (type A =<" .. type(A) .. ">)in bearingFromAtoBXY", 30) + return 0 + end + if not A.y then + trigger.action.outText("WARNING: no 'A.y' (type A =<" .. type(A) .. ">)in bearingFromAtoBXY", 30) + return 0 + end + if not B.x then + trigger.action.outText("WARNING: no 'B.x' (type B =<" .. type(B) .. ">)in bearingFromAtoBXY", 30) + return 0 + end + if not B.y then + trigger.action.outText("WARNING: no 'B.y' (type B =<" .. type(B) .. ">)in bearingFromAtoBXY", 30) + return 0 + end + + local dx = B.x - A.x + local dz = B.y - A.y + local bearing = math.atan2(dz, dx) -- in radiants + return bearing + end + function dcsCommon.bearingInDegreesFromAtoB(A, B) local bearing = dcsCommon.bearingFromAtoB(A, B) -- in rads bearing = math.floor(bearing / math.pi * 180) @@ -1116,7 +1154,7 @@ dcsCommon.version = "2.9.0" -- coalition's countries -- we start with id=0 (Russia), go to id=85 (Slovenia), but skip id = 14 local i = 0 - while i < 86 do + while i < dcsCommon.maxCountry do -- 86 do if i ~= 14 then if (coalition.getCountryCoalition(i) == aCoalition) then return i end end @@ -1125,6 +1163,22 @@ dcsCommon.version = "2.9.0" return nil end + + function dcsCommon.getCountriesForCoalition(aCoalition) + if not aCoalition then aCoalition = 0 end + local allCty = {} + + local i = 0 + while i < dcsCommon.maxCountry do + if i ~= 14 then -- there is no county 14 + if (coalition.getCountryCoalition(i) == aCoalition) then + table.insert(allCty, i) + end + end + i = i + 1 + end + return allCty + end -- -- -- C A L L B A C K H A N D L E R @@ -1394,7 +1448,7 @@ dcsCommon.version = "2.9.0" return rp end - function dcsCommon.createOverheadAirdromeRoutPintData(aerodrome) + function dcsCommon.createOverheadAirdromeRoutePointData(aerodrome) if not aerodrome then return nil end local rp = {} local p = aerodrome:getPoint() @@ -1408,6 +1462,9 @@ dcsCommon.version = "2.9.0" rp.alt_type = "BARO" return rp end + function dcsCommon.createOverheadAirdromeRoutPintData(aerodrome) -- backwards-compat to typo + return dcsCommon.createOverheadAirdromeRoutePointData(aerodrome) + end function dcsCommon.createLandAtAerodromeRoutePointData(aerodrome) @@ -1418,7 +1475,7 @@ dcsCommon.version = "2.9.0" rp.airdromeId = aerodrome:getID() rp.x = p.x rp.y = p.z - rp.alt = p.y + rp.alt = land.getHeight({x=p.x, y=p.z}) --p.y rp.action = "Landing" rp.type = "Land" @@ -1427,6 +1484,19 @@ dcsCommon.version = "2.9.0" return rp end + function dcsCommon.createSimpleRoutePointData(p, alt) + if not alt then alt = 8000 end -- 24'000 feet + local rp = {} + rp.x = p.x + rp.y = p.z + rp.alt = alt + rp.action = "Turning Point" + rp.type = "Turning Point" + + rp.speed = 133; -- in m/s? If so, that's 360 km/h + rp.alt_type = "BARO" + return rp + end function dcsCommon.createRPFormationData(findex) -- must be added as "task" to an RP. use 4 for Echelon right local task = {} @@ -2481,8 +2551,8 @@ end if not inrecursion then -- output a marker to find in the log / screen - trigger.action.outText("=== dcsCommon vardump END", 30) - env.info("=== dcsCommon vardump END") + trigger.action.outText("=== dcsCommon vardump end", 30) + env.info("=== dcsCommon vardump end") end end @@ -2520,8 +2590,8 @@ end if not inrecursion then -- output a marker to find in the log / screen - trigger.action.outText("=== dcsCommon vardump END", 30) - --env.info("=== dcsCommon vardump END") + trigger.action.outText("=== dcsCommon vardump end", 30) + --env.info("=== dcsCommon vardump end") end end @@ -2541,15 +2611,19 @@ end if id == 0 then return "invalid" end -- translate the event id to text local events = {"shot", "hit", "takeoff", "land", - "crash", "eject", "refuel", "dead", + "crash", "eject", "refuel", "dead", -- 8 "pilot dead", "base captured", "mission start", "mission end", -- 12 - "took control", "refuel stop", "birth", "human failure", - "det. failure", "engine start", "engine stop", "player enter unit", - "player leave unit", "player comment", "start shoot", "end shoot", - "mark add", "mark changed", "makr removed", "kill", - "score", "unit lost", "land after eject", "Paratrooper land", - "chair discard after eject", "weapon add", "trigger zone", "landing quality mark", - "BDA", "max"} + "took control", "refuel stop", "birth", "human failure", -- 16 + "det. failure", "engine start", "engine stop", "player enter unit", -- 20 + "player leave unit", "player comment", "start shoot", "end shoot", -- 24 + "mark add", "mark changed", "mark removed", "kill", -- 28 + "score", "unit lost", "land after eject", "Paratrooper land", -- 32 + "chair discard after eject", "weapon add", "trigger zone", "landing quality mark", -- 36 + "BDA", "AI Abort Mission", "DayNight", "Flight Time", -- 40 + "Pilot Suicide", "player cap airfield", "emergency landing", "unit create task", -- 44 + "unit delete task", "Simulation start", "weapon rearm", "weapon drop", -- 48 + "unit task timeout", "unit task stage", + "max"} if id > #events then return "Unknown (ID=" .. id .. ")" end return events[id] end diff --git a/modules/duel.lua b/modules/duel.lua new file mode 100644 index 0000000..1048bb5 --- /dev/null +++ b/modules/duel.lua @@ -0,0 +1,253 @@ +duel = {} +duel.version = "1.0.0" +duel.verbose = false +duel.requiredLibs = { + "dcsCommon", + "cfxZones", + "cfxMX", +} +--[[-- + Version History + 1.0.0 - Initial Version + +--]]-- + +--[[-- + ATTENTION! + - REQUIRES that SSB is running on the host + - REQUIRTES that SSB is confgured that '0' (zero) means slot is enabled (this is SSB default) + - This script must run at MISSION START and will enable SSB + +--]]-- + +duel.duelZones = {} +duel.activeDuelists = {} +duel.allDuelists = {} +-- +-- reading attributes +-- +function duel.createDuelZone(theZone) + theZone.duelists = {} -- all player units in this zone + -- iterate all players and find any unit that is placed in this zone + for unitName, unitData in pairs(cfxMX.playerUnitByName) do + local p = {} + p.x = unitData.x + p.z = unitData.y -- !! + p.y = 0 + + if theZone:pointInZone(p) then + -- this is a player aircraft in this zone + local duelist = {} + duelist.data = unitData + duelist.name = unitName + duelist.type = unitData.type + local groupData = cfxMX.playerUnit2Group[unitName] + duelist.groupName = groupData.name + duelist.coa = cfxMX.groupCoalitionByName[duelist.groupName] + if duel.verbose then + trigger.action.outText("Detected player unit <" .. duelist.name .. ">, type <" .. duelist.type .. "> of group <" .. duelist.groupName .. "> of coa <" .. duelist.coa .. "> in zone <" .. theZone.name .. "> as duelist", 30) + end + + duelist.active = false + duelist.arena = theZone.name + duelist.zone = theZone + + -- enter into global table + -- player can only be in at maximum one duelist zones + if duel.allDuelists[unitName] then + trigger.action.outText("+++WARNING: overlapping duelists! Overwriting previous data", 30) + end + duel.allDuelists[unitName] = duelist + theZone.duelists[unitName] = duelist + end + end + + theZone.state = "waiting" -- FSM, init to waiting state + +end + +-- +-- Event processing +-- +function duel.closeSlotsForZoneAndCoaExceptGroupNamed(theZone, coa, groupName) + -- iterate this zone's duelist groups and tell SSB to close them now + local allDuelists = theZone.duelists + for unitName, theDuelist in pairs(allDuelists) do + local dgName = theDuelist.groupName + if (theDuelist.coa == coa) and (dgName ~= groupName) then + if duel.verbose then + trigger.action.outText("+++duel: closing SSB slot for group <" .. dgName .. ">, coa <" .. theDuelist.coa .. ">", 30) + trigger.action.setUserFlag(dgName,100) -- anything but 0 means closed + end + end + end +end + + +function duel.openSlotsForZoneAndCoa(theZone, coa) + local allDuelists = theZone.duelists + for unitName, theDuelist in pairs(allDuelists) do + if (theDuelist.coa == coa) then + if duel.verbose then + trigger.action.outText("+++duel: opening SSB slot for group <" .. theDuelist.groupName .. ">, coa <" .. theDuelist.coa .. ">", 30) + trigger.action.setUserFlag(theDuelist.groupName, 0) -- 0 means OPEN + end + end + end +end + +function duel.checkReopenSlotsForZoneAndCoa(theZone, coa) + -- test if one side can reopen all slots to enter the duel + -- if so, will reset FSM for zone + local allDuelists = theZone.duelists + local allUnengaged = true + for unitName, theDuelist in pairs(allDuelists) do + if (theDuelist.coa == coa) then + local theUnit = Unit.getByName(unitName) + if theUnit and Unit.isExist(theUnit) then + -- unit is still alive on this side, can't reopen + allUnengaged = false + end + end + end + + if allUnengaged then + duel.openSlotsForZoneAndCoa(theZone, coa) + theZone.state = "waiting" + end +end + +function duel.duelistEnteredArena(theUnit, theDuelist) + -- we connect the player with duelist slot + theDuelist.playerName = theUnit:getPlayerName() + theDuelist.active = true + + local player = theUnit:getPlayerName() + local unitName = theUnit:getName() + local groupName = theDuelist.groupName + local theZone = theDuelist.zone --duel.duelZones[theDuelist.arena] + local coa = theDuelist.coa + + if duel.verbose then + trigger.action.outText("Player <" .. player .. "> entered arena <" .. theZone:getName() .. "> in unit <" .. unitName .. "> of group <" .. groupName .. "> type <" .. theDuelist.type .. ">, belongs to coalition <" .. coa .. ">", 30) + end + + -- close all slots for this zone and coalition + duel.closeSlotsForZoneAndCoaExceptGroupNamed(theZone, coa, groupName) + +end + +function duel:onEvent(event) + if not event then return end + if duel.verbose then + --trigger.action.outText("Event: " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ")", 30) + end + local theUnit = event.initiator + if not theUnit then return end + + if event.id == 15 then -- birth + local unitName = theUnit:getName() + -- see if this is a duelist that has spawned + if not duel.allDuelists[unitName] then + return -- not a duelist, not my problem + end + + -- unit that entered is player controlled, and duelist + duel.duelistEnteredArena(theUnit, duel.allDuelists[unitName]) + end +end + +-- +-- update +-- + +function duel.update() + -- call me in a second to poll triggers + timer.scheduleFunction(duel.update, {}, timer.getTime() + 1/duel.ups) + + -- find units that have disappeared, and react accordingly + for unitName, theDuelist in pairs (duel.allDuelists) do + local theZone = theDuelist.zone + if theDuelist.active then +-- trigger.action.outText("+++duel: unit <" .. unitName .. "> is active in zone <" .. theZone:getName() .. ">, controlled by <" .. theDuelist.playerName .. ">", 30) + + local theUnit = Unit.getByName(unitName) + if theUnit and Unit.isExist(theUnit) then + -- all is well + else + if duel.verbose then + trigger.action.outText("+++duel: unit <" .. unitName .. "> controlled by <" .. theDuelist.playerName .. "> has disappeared, starting cleanup", 30) + end + + theDuelist.playerName = nil + theDuelist.active = false + duel.checkReopenSlotsForZoneAndCoa(theZone, theDuelist.coa) + end + end + end + + -- now handle FSM for each zone separately + for zoneName, theZone in pairs(duel.duelZones) do + + end +end + + +-- +-- Config & start +-- +function duel.readConfigZone() + local theZone = cfxZones.getZoneByName("duelConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("duelConfig") + end + + duel.verbose = theZone.verbose + duel.ups = theZone:getNumberFromZoneProperty("ups", 1) + + duel.inside = theZone:getBoolFromZoneProperty("inside", true) + duel.gracePeriod = theZone:getNumberFromZoneProperty("gracePeriod", 30) + duel.keepScore = theZone:getBoolFromZoneProperty("score", true) + + if duel.verbose then + trigger.action.outText("+++duel: read config", 30) + end +end + +function duel.start() + if not dcsCommon.libCheck then + trigger.action.outText("cfx duel requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx Duel", duel.requiredLibs) then + return false + end + + -- turn on SSB + trigger.action.setUserFlag("SSB",100) + + -- read config + duel.readConfigZone() + + -- process cloner Zones + local attrZones = cfxZones.getZonesWithAttributeNamed("duel") + for k, aZone in pairs(attrZones) do + duel.createDuelZone(aZone) -- process attributes + duel.duelZones[aZone.name] = aZone -- add to list + end + + -- connect event handler + world.addEventHandler(duel) + + -- start update + duel.update() + + trigger.action.outText("cfx Duel v" .. duel.version .. " started.", 30) + return true + +end + +if not duel.start() then + trigger.action.outText("cfx Duel aborted: missing libraries", 30) + duel = nil +end diff --git a/modules/guardianAngel.lua b/modules/guardianAngel.lua index b5f0e1b..c98f579 100644 --- a/modules/guardianAngel.lua +++ b/modules/guardianAngel.lua @@ -1,5 +1,5 @@ guardianAngel = {} -guardianAngel.version = "3.0.4" +guardianAngel.version = "3.0.5" guardianAngel.ups = 10 guardianAngel.name = "Guardian Angel" -- just in case someone accesses .name guardianAngel.launchWarning = true -- detect launches and warn pilot @@ -61,7 +61,11 @@ guardianAngel.requiredLibs = { 3.0.3 - monitorItem() guards against loss of target (nil) 3.0.4 - launchSound attribute - interventionSound attribute - + 3.0.5 - better missiole names, their object IDs seem to have disappeared, also storing launcher name + - msgTime to control how long warnings remain on the screen + - disappear message now only on verbose + - dmlZones + This script detects missiles launched against protected aircraft an removes them when they are about to hit @@ -133,7 +137,7 @@ end -- -- watch q items -- -function guardianAngel.createQItem(theWeapon, theTarget, threat) +function guardianAngel.createQItem(theWeapon, theTarget, threat, launcher) if not theWeapon then return nil end if not theTarget then return nil end if not theTarget:isExist() then return nil end @@ -142,8 +146,26 @@ function guardianAngel.createQItem(theWeapon, theTarget, threat) -- watch it for re-targeting purposes local theItem = {} + local oName = tostring(theWeapon:getName()) + if not oName or #oName < 1 then oName = dcsCommon.numberUUID() end + local wName = "" + if theWeapon.getDisplayName then + wName = theWeapon:getDisplayName() -- does this even exist any more? + elseif theWeapon.getTypeName then + wName = theWeapon:getTypeName() + else + wName = "" + end + wName = wName .. "-" .. oName + local launcherName = launcher:getTypeName() .. " " .. launcher:getName() + theItem.theWeapon = theWeapon -- weapon that we are tracking - theItem.weaponName = theWeapon:getName() + theItem.weaponName = wName -- theWeapon:getName() + -- usually weapons have no 'name' except an ID, so let's get the + -- type or display name. Weapons often have no display name. + if guardianAngel.verbose then + trigger.action.outText("gA: tracking missile <" .. wName .. "> launched by <" .. launcherName .. ">", guardianAngel.msgTime) + end theItem.theTarget = theTarget theItem.tGroup = theTarget:getGroup() theItem.tID = theItem.tGroup:getID() @@ -156,6 +178,7 @@ function guardianAngel.createQItem(theWeapon, theTarget, threat) theItem.threat = threat theItem.lastDesc = "(new)" theItem.timeStamp = timer.getTime() + theItem.launcher = launcherName return theItem end @@ -222,14 +245,17 @@ function guardianAngel.monitorItem(theItem) if not w then return false end if not w:isExist() then --if (not theItem.missed) and (not theItem.lostTrack) then - local desc = theItem.weaponName .. ": DISAPPEARED" + + --[[-- if guardianAngel.announcer and theItem.threat then + local desc = theItem.weaponName .. ": DISAPPEARED" if guardianAngel.private then - trigger.action.outTextForGroup(ID, desc, 30) + trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime) else - trigger.action.outText(desc, 30) + trigger.action.outText(desc, guardianAngel.msgTime) end end + --]]-- if guardianAngel.verbose then trigger.action.outText("+++gA: missile disappeared: <" .. theItem.weaponName .. ">, aimed at <" .. theItem.targetName .. ">",30) end @@ -277,13 +303,13 @@ function guardianAngel.monitorItem(theItem) if isThreat and guardianAngel.announcer and guardianAngel.active then local desc = "Missile, missile, missile - now heading for " .. ctName .. "!" if guardianAngel.private then - trigger.action.outTextForGroup(ID, desc, 30) + trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime) if guardianAngel.launchSound then local fileName = "l10n/DEFAULT/" .. guardianAngel.launchSound trigger.action.outSoundForGroup(ID, fileName) end else - trigger.action.outText(desc, 30) + trigger.action.outText(desc, guardianAngel.msgTime) if guardianAngel.launchSound then local fileName = "l10n/DEFAULT/" .. guardianAngel.launchSound trigger.action.outSound(fileName) @@ -340,13 +366,13 @@ function guardianAngel.monitorItem(theItem) if guardianAngel.announcer then if guardianAngel.private then - trigger.action.outTextForGroup(ID, desc, 30) + trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime) if guardianAngel.interventionSound then local fileName = "l10n/DEFAULT/" .. guardianAngel.interventionSound trigger.action.outSoundForGroup(ID, fileName) end else - trigger.action.outText(desc, 30) + trigger.action.outText(desc, guardianAngel.msgTime) if guardianAngel.interventionSound then local fileName = "l10n/DEFAULT/" .. guardianAngel.interventionSound trigger.action.outSound(fileName) @@ -375,9 +401,9 @@ function guardianAngel.monitorItem(theItem) if guardianAngel.announcer then if guardianAngel.private then - trigger.action.outTextForGroup(ID, desc, 30) + trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime) else - trigger.action.outText(desc, 30) + trigger.action.outText(desc, guardianAngel.msgTime) end end guardianAngel.invokeCallbacks("intervention", theItem.targetName, theItem.weaponName) @@ -591,6 +617,7 @@ function guardianAngel.somethingHappened(event) -- if we get here, we have weapon aimed at a target local targetName = theTarget:getName() local watchedUnit = guardianAngel.getWatchedUnitByName(targetName) + local launcher = theUnit guardianAngel.missilesAndTargets[theWeapon:getName()] = targetName if not watchedUnit then -- we may still want to watch this if the missile @@ -599,14 +626,14 @@ function guardianAngel.somethingHappened(event) trigger.action.outText("+++gA: missile <" .. theWeapon:getName() .. "> targeting <" .. targetName .. ">, not a threat", 30) end -- add it as no threat - local theQItem = guardianAngel.createQItem(theWeapon, theTarget, false) -- this is not a threat, simply watch for re-target + local theQItem = guardianAngel.createQItem(theWeapon, theTarget, false, launcher) -- this is not a threat, simply watch for re-target table.insert(guardianAngel.missilesInTheAir, theQItem) return end -- fired at some other poor sucker, we don't care -- if we get here, someone fired a guided weapon at my watched units -- create a new item for my queue - local theQItem = guardianAngel.createQItem(theWeapon, theTarget, true) -- this is watched + local theQItem = guardianAngel.createQItem(theWeapon, theTarget, true, launcher) -- this is watched table.insert(guardianAngel.missilesInTheAir, theQItem) guardianAngel.invokeCallbacks("launch", theQItem.targetName, theQItem.weaponName) @@ -624,13 +651,13 @@ function guardianAngel.somethingHappened(event) -- currently, we always detect immediately -- can be moved to update() if guardianAngel.private then - trigger.action.outTextForGroup(grpID, "Missile, missile, missile, " .. oclock .. " o clock" .. vbInfo, 30) + trigger.action.outTextForGroup(grpID, "Missile, missile, missile, " .. oclock .. " o clock" .. vbInfo, guardianAngel.msgTime) if guardianAngel.launchSound then local fileName = "l10n/DEFAULT/" .. guardianAngel.launchSound trigger.action.outSoundForGroup(grpID, fileName) end else - trigger.action.outText("Missile, missile, missile, " .. oclock .. " o clock" .. vbInfo, 30) + trigger.action.outText("Missile, missile, missile, " .. oclock .. " o clock" .. vbInfo, guardianAngel.msgTime) if guardianAngel.launchSound then local fileName = "l10n/DEFAULT/" .. guardianAngel.launchSound trigger.action.outSound(fileName) @@ -847,40 +874,42 @@ function guardianAngel.readConfigZone() end - guardianAngel.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + guardianAngel.verbose = theZone:getBoolFromZoneProperty("verbose", false) - guardianAngel.autoAddPlayer = cfxZones.getBoolFromZoneProperty(theZone, "autoAddPlayer", true) - guardianAngel.launchWarning = cfxZones.getBoolFromZoneProperty(theZone, "launchWarning", true) - guardianAngel.intervention = cfxZones.getBoolFromZoneProperty(theZone, "intervention", true) - guardianAngel.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true) - guardianAngel.private = cfxZones.getBoolFromZoneProperty(theZone, "private", false) - guardianAngel.explosion = cfxZones.getNumberFromZoneProperty(theZone, "explosion", -1) - guardianAngel.fxDistance = cfxZones.getNumberFromZoneProperty(theZone, "fxDistance", 500) + guardianAngel.autoAddPlayer = theZone:getBoolFromZoneProperty("autoAddPlayer", true) + guardianAngel.launchWarning = theZone:getBoolFromZoneProperty("launchWarning", true) + guardianAngel.intervention = theZone:getBoolFromZoneProperty("intervention", true) + guardianAngel.announcer = theZone:getBoolFromZoneProperty( "announcer", true) + guardianAngel.private = theZone:getBoolFromZoneProperty("private", false) + guardianAngel.explosion = theZone:getNumberFromZoneProperty("explosion", -1) + guardianAngel.fxDistance = theZone:getNumberFromZoneProperty( "fxDistance", 500) - guardianAngel.active = cfxZones.getBoolFromZoneProperty(theZone, "active", true) + guardianAngel.active = theZone:getBoolFromZoneProperty("active", true) - if cfxZones.hasProperty(theZone, "activate?") then - guardianAngel.activate = cfxZones.getStringFromZoneProperty(theZone, "activate?", "*") - guardianAngel.lastActivate = cfxZones.getFlagValue(guardianAngel.activate, theZone) - elseif cfxZones.hasProperty(theZone, "on?") then - guardianAngel.activate = cfxZones.getStringFromZoneProperty(theZone, "on?", "*") - guardianAngel.lastActivate = cfxZones.getFlagValue(guardianAngel.activate, theZone) + guardianAngel.msgTime = theZone:getNumberFromZoneProperty("msgTime", 30) + + if theZone:hasProperty("activate?") then + guardianAngel.activate = theZone:getStringFromZoneProperty("activate?", "*") + guardianAngel.lastActivate = theZone:getFlagValue(guardianAngel.activate) + elseif theZone:hasProperty("on?") then + guardianAngel.activate = theZone:getStringFromZoneProperty("on?", "*") + guardianAngel.lastActivate = theZone:getFlagValue(guardianAngel.activate) end - if cfxZones.hasProperty(theZone, "deactivate?") then - guardianAngel.deactivate = cfxZones.getStringFromZoneProperty(theZone, "deactivate?", "*") - guardianAngel.lastDeActivate = cfxZones.getFlagValue(guardianAngel.deactivate, theZone) - elseif cfxZones.hasProperty(theZone, "off?") then - guardianAngel.deactivate = cfxZones.getStringFromZoneProperty(theZone, "off?", "*") - guardianAngel.lastDeActivate = cfxZones.getFlagValue(guardianAngel.deactivate, theZone) + if theZone:hasProperty("deactivate?") then + guardianAngel.deactivate = theZone:getStringFromZoneProperty("deactivate?", "*") + guardianAngel.lastDeActivate = theZone:getFlagValue(guardianAngel.deactivate) + elseif theZone:hasProperty("off?") then + guardianAngel.deactivate = theZone:getStringFromZoneProperty("off?", "*") + guardianAngel.lastDeActivate = theZone:getFlagValue(guardianAngel.deactivate) end - if cfxZones.hasProperty(theZone, "launchSound") then - guardianAngel.launchSound = cfxZones.getStringFromZoneProperty(theZone, "launchSound", "nosound") + if theZone:hasProperty("launchSound") then + guardianAngel.launchSound = theZone:getStringFromZoneProperty("launchSound", "nosound") end - if cfxZones.hasProperty(theZone, "interventionSound") then - guardianAngel.interventionSound = cfxZones.getStringFromZoneProperty(theZone, "interventionSound", "nosound") + if theZone:hasProperty("interventionSound") then + guardianAngel.interventionSound = theZone:getStringFromZoneProperty("interventionSound", "nosound") end guardianAngel.configZone = theZone @@ -894,7 +923,7 @@ end -- function guardianAngel.processGuardianZone(theZone) - theZone.angelic = cfxZones.getBoolFromZoneProperty(theZone, "guardian", true) + theZone.angelic = theZone:getBoolFromZoneProperty("guardian", true) if theZone.verbose or guardianAngel.verbose then @@ -947,7 +976,7 @@ function guardianAngel.start() end function guardianAngel.testCB(reason, targetName, weaponName) - trigger.action.outText("gA - CB for ".. reason .. ": " .. targetName .. " w: " .. weaponName, 30) + trigger.action.outText("gA - CB for ".. reason .. ": " .. targetName .. " w: " .. weaponName, guardianAngel.msgTime) end -- go go go diff --git a/modules/unitZone.lua b/modules/unitZone.lua index c318f33..a82a87a 100644 --- a/modules/unitZone.lua +++ b/modules/unitZone.lua @@ -1,5 +1,5 @@ unitZone={} -unitZone.version = "1.2.4" +unitZone.version = "1.2.5" unitZone.verbose = false unitZone.ups = 1 unitZone.requiredLibs = { @@ -17,6 +17,7 @@ unitZone.requiredLibs = { 1.2.3 - better guards for enterZone!, exitZone!, changeZone! - better guards for uzOn? and uzOff? 1.2.4 - more verbosity on uzDirect + 1.2.5 - reading config improvement --]]-- @@ -196,8 +197,9 @@ function unitZone.checkZoneStatus(theZone) local playerCheck = theZone.matching == "player" if playerCheck then -- we check the names for players only + -- collector holds units, not groups for idx, pUnit in pairs(theGroups) do - local puName=pUnit:getName() + local puName = pUnit:getName() local hasMatch = false if theZone.lookForBeginsWith then hasMatch = dcsCommon.stringStartsWith(puName, lookFor) @@ -323,10 +325,7 @@ end function unitZone.readConfigZone() local theZone = cfxZones.getZoneByName("unitZoneConfig") if not theZone then - if unitZone.verbose then - trigger.action.outText("+++uZne: NO config zone!", 30) - end - return + theZone = cfxZones.createSimpleZone("unitZoneConfig") end unitZone.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) diff --git a/tutorial & demo missions/demo - Civ Air International.miz b/tutorial & demo missions/demo - Civ Air International.miz new file mode 100644 index 0000000..da699b7 Binary files /dev/null and b/tutorial & demo missions/demo - Civ Air International.miz differ diff --git a/tutorial & demo missions/demo - recon mode - reloaded.miz b/tutorial & demo missions/demo - recon mode - reloaded.miz index 62d9bf3..93ab49e 100644 Binary files a/tutorial & demo missions/demo - recon mode - reloaded.miz and b/tutorial & demo missions/demo - recon mode - reloaded.miz differ