diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index fb0cef7..a9f290f 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 ed69d73..ccda002 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/bank.lua b/modules/bank.lua index 59675f8..e59f9b0 100644 --- a/modules/bank.lua +++ b/modules/bank.lua @@ -147,4 +147,9 @@ end if not bank.start() then trigger.action.outText("bank aborted: missing libraries", 30) bank = nil -end \ No newline at end of file +end + +--[[-- + Add 'creditLine' input to directly load a value into the bank? + +--]]-- diff --git a/modules/bombRange.lua b/modules/bombRange.lua index 8c6815b..7faacd8 100644 --- a/modules/bombRange.lua +++ b/modules/bombRange.lua @@ -1,5 +1,5 @@ bombRange = {} -bombRange.version = "1.1.3" +bombRange.version = "2.0.0" bombRange.dh = 1 -- meters above ground level burst bombRange.requiredLibs = { @@ -22,6 +22,11 @@ VERSION HISTORY minor clean-up 1.1.2 - corrected bug when no bomb range is detected 1.1.3 - added meters/feet distance when reporting impact +1.1.4 - code hardening against CA interference +2.0.0 - support for radioMainMenu + - support for types + - types can have wild cards + --]]-- bombRange.bombs = {} -- live tracking @@ -170,6 +175,10 @@ function bombRange.showStatsForPlayer(pName, gID, unitName) if bombRange.mustCheckIn then local comms = bombRange.unitComms[unitName] + if not comms then + -- player controlled CA vehicle calling. go away. + return + end if comms.checkedIn then msg = msg .. "\nYou are checked in with weapons range command.\n" else @@ -184,6 +193,11 @@ end -- unit UI -- function bombRange.initCommsForUnit(theUnit) + local mainMenu = nil + if bombRange.mainMenu then + mainMenu = radioMenu.getMainMenuFor(bombRange.mainMenu) -- nilling both next params will return menus[0] + end + local uName = theUnit:getName() local pName = theUnit:getPlayerName() local theGroup = theUnit:getGroup() @@ -199,7 +213,7 @@ function bombRange.initCommsForUnit(theUnit) end comms = {} comms.checkedIn = false - comms.root = missionCommands.addSubMenuForGroup(gID, bombRange.menuTitle) + comms.root = missionCommands.addSubMenuForGroup(gID, bombRange.menuTitle, mainMenu) comms.getStat = missionCommands.addCommandForGroup(gID, "Get statistics for " .. pName, comms.root, bombRange.redirectComms, {"getStat", uName, pName, gID}) comms.reset = missionCommands.addCommandForGroup(gID, "RESET statistics for " .. pName, comms.root, bombRange.redirectComms, {"reset", uName, pName, gID}) if bombRange.mustCheckIn then @@ -231,6 +245,10 @@ function bombRange.commsRequest(args) if command == "check" then comms = bombRange.unitComms[uName] + if not comms then + -- CA player here. we don't talk to you (yet) + return + end if comms.checkedIn then comms.checkedIn = false -- we are now checked out missionCommands.removeItemForGroup(gID, comms.checkin) @@ -424,6 +442,10 @@ function bombRange:onEvent(event) if event.id == 1 then -- shot event, from player if not event.weapon then return end local uComms = bombRange.unitComms[uName] + if not uComms then + -- this is a player-controlled CA vehicle. bye bye + return + end if bombRange.mustCheckIn and (not uComms.checkedIn) then if bombRange.verbose then trigger.action.outText("+++bRng: Player <" .. pName .. "> not checked in.", 30) @@ -459,12 +481,39 @@ function bombRange:onEvent(event) end end - if event.id == 15 then + if event.id == 15 then -- birth + + if bombRange.types then + if not bombRange.typeCheck(theUnit) then return end + end + + -- we could add unit filtering by group here, e.g. only some + -- planes, defined in config + -- for example, bomb range is silly for most helicopter bombRange.initCommsForUnit(theUnit) + end end +function bombRange.typeCheck(theUnit) + local theGroup = theUnit:getGroup() + local cat = theGroup:getCategory() -- we use group, not unit. Airplane is 0, Heli is 1 + -- check the type explicitly + local myType = theUnit:getTypeName() + if dcsCommon.wildArrayContainsString(bombRange.types, myType) then return true end + + -- planes or plane perhaps? +-- if cat == 0 and dcsCommon.arrayContainsStringCaseInsensitive(bombRange.types, "planes") then return true end + if cat == 0 and dcsCommon.wildArrayContainsString(bombRange.types, "plan*") then return true end + + -- helos? + if cat == 1 and dcsCommon.wildArrayContainsString(bombRange.types, "hel*") then return true end +-- if cat == 1 and dcsCommon.arrayContainsStringCaseInsensitive(bombRange.types, "helos") then return true end +-- if cat == 1 and dcsCommon.arrayContainsStringCaseInsensitive(bombRange.types, "helicopter") then return true end + + return false +end -- -- Update -- @@ -688,6 +737,25 @@ function bombRange.readConfigZone() bombRange.signOut = theZone:getStringFromZoneProperty("signOut!", 30) end bombRange.method = theZone:getStringFromZoneProperty("method", "inc") + + if theZone:hasProperty("types") then + bombRange.types = theZone:getListFromZoneProperty("types", "") + end + + if theZone:hasProperty("attachTo:") then + local attachTo = theZone:getStringFromZoneProperty("attachTo:", "") + if radioMenu then + local mainMenu = radioMenu.mainMenus[attachTo] + if mainMenu then + bombRange.mainMenu = mainMenu + else + trigger.action.outText("+++bombRange: cannot find super menu <" .. attachTo .. ">", 30) + end + else + trigger.action.outText("+++bombRange: REQUIRES radioMenu to run before bombRange. 'AttachTo:' ignored.", 30) + end + end + bombRange.verbose = theZone.verbose end diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index a5bc1e2..d782dd6 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "4.3.2" +cfxZones.version = "4.3.4" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -51,7 +51,8 @@ cfxZones.version = "4.3.2" - 4.3.1 - new drawText() for zones - dmlZones:getClosestZone() bridge - 4.3.2 - new getListFromZoneProperty() - +- 4.3.3 - hardened calculateZoneBounds +- 4.3.4 - rewrote zone bounds for poly zones --]]-- -- @@ -221,7 +222,7 @@ function cfxZones.calculateZoneBounds(theZone) if not (theZone) then return end - local bounds = theZone.bounds -- copy ref! + local bounds = theZone.bounds -- copy ref! -- DON'T BELIEVE THIS! if theZone.isCircle then -- aabb are easy: center +/- radius @@ -234,38 +235,32 @@ function cfxZones.calculateZoneBounds(theZone) bounds.ll = dcsCommon.createPoint(center.x - radius, 0, center.z + radius) bounds.lr = dcsCommon.createPoint(center.x + radius, 0, center.z + radius) + -- write back + theZone.bounds = bounds elseif theZone.isPoly then local poly = theZone.poly -- ref copy! -- create the four points - local ll = cfxZones.createPointFromPoint(poly[1]) - local lr = cfxZones.createPointFromPoint(poly[1]) - local ul = cfxZones.createPointFromPoint(poly[1]) - local ur = cfxZones.createPointFromPoint(poly[1]) - + local p = cfxZones.createPointFromPoint(poly[1]) local pRad = dcsCommon.dist(theZone.point, poly[1]) -- rRad is radius for polygon from theZone.point -- now iterate through all points and adjust bounds accordingly - for v=2, #poly do - local vertex = poly[v] - if (vertex.x < ll.x) then ll.x = vertex.x; ul.x = vertex.x end - if (vertex.x > lr.x) then lr.x = vertex.x; ur.x = vertex.x end - if (vertex.z < ul.z) then ul.z = vertex.z; ur.z = vertex.z end - --if (vertex.z > ll.z) then ll.z = vertex.z; lr.z = vertex.z end - if (vertex.z > ur.z) then ur.z = vertex.z; ul.z = vertex.z end - local dp = dcsCommon.dist(theZone.point, vertex) + local lx, ly, mx, my = p.x, p.z, p.x, p.z + for vtx=1, #poly do + local v = poly[vtx] + if v.x < lx then lx = v.x end + if v.x > mx then mx = v.x end + if v.z < ly then ly = v.z end + if v.z > my then my = v.z end + local dp = dcsCommon.dist(theZone.point, v) if dp > pRad then pRad = dp end -- find largst distance to vertex end - -- now keep the new point references - -- and store them in the zone's bounds - bounds.ll = ll - bounds.lr = lr - bounds.ul = ul - bounds.ur = ur - -- we may need to ascertain why we need ul, ur, ll, lr instead of just ll and ur + theZone.bounds.ul = dcsCommon.createPoint(lx, 0, my) + theZone.bounds.ur = dcsCommon.createPoint(mx, 0, my) + theZone.bounds.ll = dcsCommon.createPoint(lx, 0, ly) + theZone.bounds.lr = dcsCommon.createPoint(mx, 0, ly) -- store pRad theZone.pRad = pRad -- not sure we'll ever need that, but at least we have it - else -- huston, we have a problem if cfxZones.verbose then diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index 27001c4..e0f0c73 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,5 +1,5 @@ cloneZones = {} -cloneZones.version = "2.2.1" +cloneZones.version = "2.3.0" cloneZones.verbose = false cloneZones.requiredLibs = { "dcsCommon", -- always @@ -52,6 +52,10 @@ cloneZones.respawnOnGroupID = true - persistence: persist oSize and set lastSize 2.2.1 - verbosity updates for post-check - if cloned group is late activation, turn it off + 2.3.0 - added optional cWipe? attribute to resolve possible conflict + (undocumented, just to provide lazy people with a migration + path) with wiper module + - using "wipe?" will now create a warning --]]-- -- @@ -238,6 +242,11 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" theZone.deSpawnFlag = theZone:getStringFromZoneProperty( "deClone?", "none") elseif theZone:hasProperty("wipe?") then theZone.deSpawnFlag = theZone:getStringFromZoneProperty("wipe?", "none") + trigger.action.outText("+++clnZ: WARNING - Clone Zone <" .. theZone.name .. ">: attribute 'wipe?' is deprecated for clone zones!", 30) + -- note possible conflict with wiper module, so we add the new + -- cWipe? attribute + elseif theZone:hasProperty("cWipe?") then + theZone.deSpawnFlag = theZone:getStringFromZoneProperty("cWipe?", "none") end if theZone.deSpawnFlag then diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index 2217b94..7c46503 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -1,5 +1,5 @@ csarManager = {} -csarManager.version = "3.4.0" +csarManager.version = "4.0.0" csarManager.ups = 1 --[[-- VERSION HISTORY @@ -45,7 +45,8 @@ csarManager.ups = 1 3.3.0 - persistence support 3.4.0 - global timeLimit option in config zone - fixes expiration bug when persisting data - + 4.0.0 - support for mainMenu + INTEGRATES AUTOMATICALLY WITH playerScore INTEGRATES WITH LIMITED AIRFRAMES @@ -713,8 +714,13 @@ function csarManager.setCommsMenu(theUnit) -- reset all coms now csarManager.removeCommsFromConfig(conf) + local mainMenu = nil + if csarManager.mainMenu then + mainMenu = radioMenu.getMainMenuFor(csarManager.mainMenu) -- nilling both next params will return menus[0] + end + -- ok, first, if we don't have an F-10 menu, create one - conf.myMainMenu = missionCommands.addSubMenuForGroup(id, 'CSAR Missions') + conf.myMainMenu = missionCommands.addSubMenuForGroup(id, 'CSAR Missions', mainMenu) -- now we have a menu without submenus. -- add our own submenus @@ -1618,7 +1624,22 @@ function csarManager.readConfigZone() else csarManager.timeLimit = nil end - + + + if theZone:hasProperty("attachTo:") then + local attachTo = theZone:getStringFromZoneProperty("attachTo:", "") + if radioMenu then + local mainMenu = radioMenu.mainMenus[attachTo] + if mainMenu then + csarManager.mainMenu = mainMenu + else + trigger.action.outText("+++csarManager: cannot find super menu <" .. attachTo .. ">", 30) + end + else + trigger.action.outText("+++csarManager: REQUIRES radioMenu to run before csarManager. 'AttachTo:' ignored.", 30) + end + end + if csarManager.verbose then trigger.action.outText("+++csar: read config", 30) end diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index c7dbbe2..0961c2e 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "3.0.7" +dcsCommon.version = "3.0.8" --[[-- VERSION HISTORY 3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false - point2text new intsOnly option @@ -20,7 +20,10 @@ dcsCommon.version = "3.0.7" - new pointXpercentYdegOffAB() 3.0.6 - new arrayContainsStringCaseInsensitive() 3.0.7 - fixed small bug in wildArrayContainsString - +3.0.8 - deepCopy() and deepTableCopy() alternates to clone() to patch + around a strange DCS 2.9 issue + Kiowa added to Troop Carriers + --]]-- -- dcsCommon is a library of common lua functions @@ -33,7 +36,7 @@ dcsCommon.version = "3.0.7" -- globals 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.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P", "OH58D"} -- Ka-50, Apache and Gazelle can't carry troops dcsCommon.coalitionSides = {0, 1, 2} dcsCommon.maxCountry = 86 -- number of countries defined in total @@ -1200,6 +1203,7 @@ dcsCommon.version = "3.0.7" -- clone is a recursive clone which will also clone -- deeper levels, as used in units function dcsCommon.clone(orig, stripMeta) + --[[-- code seems to break with DCS 2.9 if not orig then return nil end local orig_type = type(orig) local copy @@ -1226,7 +1230,57 @@ dcsCommon.version = "3.0.7" copy = orig end return copy + --]]-- + if stripMeta then + trigger.action.outText("+++common: warning: stripmetatable no longer supported.", 30) + end + return dcsCommon.deepTableCopy(orig) end + + -- deepCopy from http://lua-users.org/wiki/CopyTable + function dcsCommon.deepcopy(orig, copies) + if not orig then return nil end + copies = copies or {} + local orig_type = type(orig) + local copy + if orig_type == 'table' then + if copies[orig] then + copy = copies[orig] + else + copy = {} + copies[orig] = copy + for orig_key, orig_value in next, orig, nil do + copy[deepcopy(orig_key, copies)] = deepcopy(orig_value, copies) + end + setmetatable(copy, deepcopy(getmetatable(orig), copies)) + end + else -- number, string, boolean, etc + copy = orig + end + return copy + end + + -- deepTableCopy from Mist (thanks Grimes!) + function dcsCommon.deepTableCopy(object) + if not object then return nil end + local lookup_table = {} + local function _copy(object) + if type(object) ~= "table" then + return object + elseif lookup_table[object] then + return lookup_table[object] -- break cycles + end + local new_table = {} + lookup_table[object] = new_table + for index, value in pairs(object) do + new_table[_copy(index)] = _copy(value) + end + return setmetatable(new_table, getmetatable(object)) + end + return _copy(object) + end + + function dcsCommon.copyArray(inArray) if not inArray then return nil end diff --git a/modules/flareZone.lua b/modules/flareZone.lua index 174442a..e023f33 100644 --- a/modules/flareZone.lua +++ b/modules/flareZone.lua @@ -1,5 +1,5 @@ flareZone = {} -flareZone.version = "1.1.0" +flareZone.version = "1.2.0" flareZone.verbose = false flareZone.name = "flareZone" @@ -8,6 +8,7 @@ flareZone.name = "flareZone" 1.1.0 - improvements to verbosity - OOP - small bugfix in doFlare assignment + 1.2.0 - new rndLoc attribute --]]-- flareZone.requiredLibs = { "dcsCommon", @@ -50,6 +51,8 @@ function flareZone.addFlareZone(theZone) theZone.salvoDurationL, theZone.salvoDurationH = theZone:getPositiveRangeFromZoneProperty("duration", 1) + theZone.rndLoc = theZone:getBoolFromZoneProperty("rndLoc", flase) + if theZone.verbose or flareZone.verbose then trigger.action.outText("+++flrZ: new flare <" .. theZone.name .. ">, color (" .. theZone.flareColor .. ")", 30) end @@ -59,15 +62,20 @@ end function flareZone.launch(theZone) local color = theZone.flareColor if color < 0 then color = math.random(4) - 1 end - - local loc = cfxZones.getPoint(theZone, true) -- with height + local loc + if theZone.rndLoc then + locFlat = theZone:randomPointInZone() + loc = {x = locFlat.x, y = land.getHeight({x = locFlat.x, y = locFlat.z}), z = locFlat.z} + else + loc = cfxZones.getPoint(theZone, true) -- with height + end loc.y = loc.y + theZone.flareAlt -- calculate azimuth local azimuth = cfxZones.randomInRange(theZone.azimuthL, theZone.azimuthH) -- in deg - if flareZone.verbose or theZone.verbose then - trigger.action.outText("+++flrZ: launching <" .. theZone.name .. ">, c = " .. color .. " (" .. dcsCommon.flareColor2Text(color) .. "), azi <" .. azimuth .. "> [" .. theZone.azimuthL .. "-" .. theZone.azimuthH .. "]", 30) - end +-- if flareZone.verbose or theZone.verbose then +-- trigger.action.outText("+++flrZ: launching <" .. theZone.name .. ">, c = " .. color .. " (" .. dcsCommon.flareColor2Text(color) .. "), azi <" .. azimuth .. "> [" .. theZone.azimuthL .. "-" .. theZone.azimuthH .. "]", 30) +-- end azimuth = azimuth * 0.0174533 -- in rads trigger.action.signalFlare(loc, color, azimuth) @@ -80,6 +88,9 @@ function flareZone.update() -- launch if flag banged for idx, theZone in pairs(flareZone.flares) do if cfxZones.testZoneFlag(theZone, theZone.doFlare, theZone.flareTriggerMethod, "lastDoFlare") then + if flareZone.verbose or theZone.verbose then + trigger.action.outText("+++flr: triggerd flares for <" .. theZone.name .. "> on input? <" .. theZone.doFlare .. ">", 30) + end local salvo = cfxZones.randomInRange(theZone.salvoSizeL, theZone.salvoSizeH) if salvo < 2 then -- one-shot diff --git a/modules/groundTroops.lua b/modules/groundTroops.lua index b9123d7..54d637e 100644 --- a/modules/groundTroops.lua +++ b/modules/groundTroops.lua @@ -1,5 +1,5 @@ cfxGroundTroops = {} -cfxGroundTroops.version = "2.2.0" +cfxGroundTroops.version = "2.2.1" cfxGroundTroops.ups = 0.25 -- every 4 seconds cfxGroundTroops.verbose = false cfxGroundTroops.requiredLibs = { @@ -34,6 +34,7 @@ cfxGroundTroops.jtacCB = {} -- jtac callbacks, to be implemented 2.0.1 - small fiex ti checkPileUp() 2.1.0 - captureandhold - oneshot attackowned 2.2.0 - moveFormation support + 2.2.1 - reduced verbosity an entry into the deployed troop table has the following attributes - group - the group @@ -926,7 +927,7 @@ function cfxGroundTroops.createGroundTroops(inGroup, range, orders, moveFormatio if orders:lower() == "lase" then orders = "laze" -- we use WRONG spelling here, cause we're cool. yeah, right. end - trigger.action.outText("Enter createGT group <" .. inGroup:getName() .. "> with o=<" .. orders .. ">, mf=<" .. moveFormation .. ">", 30) +-- trigger.action.outText("Enter createGT group <" .. inGroup:getName() .. "> with o=<" .. orders .. ">, mf=<" .. moveFormation .. ">", 30) newTroops.insideDestination = false newTroops.unscheduleCount = 0 -- will count up as we aren't scheduled newTroops.speedWarning = 0 diff --git a/modules/guardianAngel.lua b/modules/guardianAngel.lua index c98f579..07d6ad9 100644 --- a/modules/guardianAngel.lua +++ b/modules/guardianAngel.lua @@ -1,5 +1,5 @@ guardianAngel = {} -guardianAngel.version = "3.0.5" +guardianAngel.version = "3.0.6" guardianAngel.ups = 10 guardianAngel.name = "Guardian Angel" -- just in case someone accesses .name guardianAngel.launchWarning = true -- detect launches and warn pilot @@ -65,6 +65,7 @@ guardianAngel.requiredLibs = { - msgTime to control how long warnings remain on the screen - disappear message now only on verbose - dmlZones + 3.0.6 - Hardening of targets that aren't part of groups This script detects missiles launched against protected aircraft an @@ -167,9 +168,13 @@ function guardianAngel.createQItem(theWeapon, theTarget, threat, launcher) 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() - + if theTarget.getGroup then -- some targets may not have a group + theItem.tGroup = theTarget:getGroup() + theItem.tID = theItem.tGroup:getID() + else + theItem.tGroup = nil + theItem.tID = nil + end theItem.targetName = theTarget:getName() theItem.launchTimeStamp = timer.getTime() theItem.lastDistance = math.huge @@ -244,24 +249,10 @@ function guardianAngel.monitorItem(theItem) local ID = theItem.tID if not w then return false end if not w:isExist() then - --if (not theItem.missed) and (not theItem.lostTrack) then - - --[[-- - if guardianAngel.announcer and theItem.threat then - local desc = theItem.weaponName .. ": DISAPPEARED" - if guardianAngel.private then - trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime) - else - 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 - - guardianAngel.invokeCallbacks("disappear", theItem.targetName, theItem.weaponName) - -- end + if guardianAngel.verbose then + trigger.action.outText("+++gA: missile disappeared: <" .. theItem.weaponName .. ">, aimed at <" .. theItem.targetName .. ">",30) + end + guardianAngel.invokeCallbacks("disappear", theItem.targetName, theItem.weaponName) return false end @@ -302,7 +293,7 @@ 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 + if guardianAngel.private and ID then trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime) if guardianAngel.launchSound then local fileName = "l10n/DEFAULT/" .. guardianAngel.launchSound @@ -364,7 +355,7 @@ function guardianAngel.monitorItem(theItem) then desc = desc .. " ANGEL INTERVENTION" - if guardianAngel.announcer then + if guardianAngel.announcer and ID then if guardianAngel.private then trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime) if guardianAngel.interventionSound then @@ -399,7 +390,7 @@ function guardianAngel.monitorItem(theItem) --if theItem.lostTrack then desc = desc .. " (little sneak!)" end --if theItem.missed then desc = desc .. " (missed you!)" end - if guardianAngel.announcer then + if guardianAngel.announcer and ID then if guardianAngel.private then trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime) else diff --git a/modules/jtacGrpUI.lua b/modules/jtacGrpUI.lua index ec739f2..a8dc9c4 100644 --- a/modules/jtacGrpUI.lua +++ b/modules/jtacGrpUI.lua @@ -1,5 +1,5 @@ jtacGrpUI = {} -jtacGrpUI.version = "2.0.0" +jtacGrpUI.version = "3.0.0" jtacGrpUI.requiredLibs = { "dcsCommon", -- always "cfxZones", @@ -11,6 +11,8 @@ jtacGrpUI.requiredLibs = { - eliminated cfxPlayer dependence - clean-up - jtacSound + 3.0.0 - support for attachTo: + --]]-- -- find & command cfxGroundTroops-based jtacs -- UI installed via OTHER for all groups with players @@ -143,6 +145,11 @@ function jtacGrpUI.setCommsMenu(theGroup) if not Group.isExist(theGroup) then return end if not jtacGrpUI.isEligibleForMenu(theGroup) then return end + local mainMenu = nil + if jtacGrpUI.mainMenu then + mainMenu = radioMenu.getMainMenuFor(jtacGrpUI.mainMenu) -- nilling both next params will return menus[0] + end + local conf = jtacGrpUI.getConfigForGroup(theGroup) conf.id = theGroup:getID(); -- we always do this ALWAYS @@ -151,7 +158,7 @@ function jtacGrpUI.setCommsMenu(theGroup) if not conf.myMainMenu then local commandTxt = "jtac Lasing Report" local theCommand = missionCommands.addCommandForGroup( - conf.id, commandTxt, nil, jtacGrpUI.redirectCommandX, {conf, "lasing report"}) + conf.id, commandTxt, mainMenu, jtacGrpUI.redirectCommandX, {conf, "lasing report"}) conf.myMainMenu = theCommand end @@ -160,7 +167,7 @@ function jtacGrpUI.setCommsMenu(theGroup) -- ok, first, if we don't have an F-10 menu, create one if not (conf.myMainMenu) then - conf.myMainMenu = missionCommands.addSubMenuForGroup(conf.id, 'jtac') + conf.myMainMenu = missionCommands.addSubMenuForGroup(conf.id, 'jtac', mainMenu) end -- clear out existing commands @@ -317,12 +324,27 @@ function jtacGrpUI.readConfigZone() if not theZone then theZone = cfxZones.createSimpleZone("jtacGrpUIConfig") end - + jtacGrpUI.name = "jtacGrpUI" + jtacGrpUI.jtacTypes = theZone:getStringFromZoneProperty("jtacTypes", "all") jtacGrpUI.jtacTypes = string.lower(jtacGrpUI.jtacTypes) jtacGrpUI.jtacSound = theZone:getStringFromZoneProperty("jtacSound", "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") - + + if theZone:hasProperty("attachTo:") then + local attachTo = theZone:getStringFromZoneProperty("attachTo:", "") + if radioMenu then + local mainMenu = radioMenu.mainMenus[attachTo] + if mainMenu then + jtacGrpUI.mainMenu = mainMenu + else + trigger.action.outText("+++jtacGrpUI: cannot find super menu <" .. attachTo .. ">", 30) + end + else + trigger.action.outText("+++jtacGrpUI: REQUIRES radioMenu to run before jtacGrpUI. 'AttachTo:' ignored.", 30) + end + end + jtacGrpUI.verbose = theZone.verbose end diff --git a/modules/launchPlatform.lua b/modules/launchPlatform.lua index 5f9bd2d..b2eed85 100644 --- a/modules/launchPlatform.lua +++ b/modules/launchPlatform.lua @@ -1,5 +1,5 @@ launchPlatform = {} -launchPlatform.version = "0.0.0" +launchPlatform.version = "0.5.0" launchPlatform.requiredLibs = { "dcsCommon", "cfxZones", @@ -55,7 +55,9 @@ function launchPlatform.launchAtTargetZone(coa, tgtZone, theType) -- gets closes -- get closest launcher for target local tgtPoint = tgtZone:getPoint() local src, dist = cfxZones.getClosestZone(tgtPoint, platforms) - trigger.action.outText("+++LP: chosen <" .. src.name .. "> as launch platform", 30) + if launchPlatform.verbose then + trigger.action.outText("+++LP: chosen <" .. src.name .. "> as launch platform", 30) + end local theLauncher = launchPlatform.launchForPlatform(coa, src, tgtPoint, tgtZone) if not theLauncher then @@ -74,7 +76,9 @@ function launchPlatform.launchAtTargetZone(coa, tgtZone, theType) -- gets closes end function launchPlatform.asynchRemovePlatform(args) - trigger.action.outText("LP: asynch remove for group <" .. args .. ">", 30) + if launchPlatform.verbose then + trigger.action.outText("+++LP: asynch remove for group <" .. args .. ">", 30) + end local theGroup = Group.getByName(args) if not theGroup then return end Group.destroy(theGroup) @@ -83,11 +87,11 @@ end function launchPlatform.createData(thePoint, theTarget, targetZone, radius, name, num, wType) -- if present, we can use targetZone with some intelligence if not thePoint then - trigger.action.outText("NO POINT", 30) + trigger.action.outText("+++LP: NO POINT", 30) return nil end if not theTarget then - trigger.action.outText("NO TARGET", 30) + trigger.action.outText("+++LP: NO TARGET", 30) return nil end @@ -168,7 +172,9 @@ function launchPlatform.createData(thePoint, theTarget, targetZone, radius, name -- if inside camp local hiPrioTargets if targetZone and targetZone.cloners and #targetZone.cloners > 0 then - trigger.action.outText("+++LP: detected <" .. targetZone.name .. "> is camp with <" .. #targetZone.cloners .. "> res-points, re-targeting hi-prio", 30) + if launchPlatform.verbose then + trigger.action.outText("+++LP: detected <" .. targetZone.name .. "> is camp with <" .. #targetZone.cloners .. "> res-points, re-targeting hi-prio", 30) + end hiPrioTargets = targetZone.cloners radius = radius / 10 -- much smaller error end diff --git a/modules/milWings.lua b/modules/milWings.lua index 48b563b..9951c01 100644 --- a/modules/milWings.lua +++ b/modules/milWings.lua @@ -406,7 +406,7 @@ end -- Event Handler -- function milWings:onEvent(theEvent) -if not theEvent then return end + if not theEvent then return end if not theEvent.initiator then return end local theUnit = theEvent.initiator if not theUnit.getGroup then return end diff --git a/modules/playerScore.lua b/modules/playerScore.lua index be1d492..a13472b 100644 --- a/modules/playerScore.lua +++ b/modules/playerScore.lua @@ -1,5 +1,5 @@ cfxPlayerScore = {} -cfxPlayerScore.version = "3.2.0" +cfxPlayerScore.version = "3.3.0" cfxPlayerScore.name = "cfxPlayerScore" -- compatibility with flag bangers cfxPlayerScore.badSound = "Death BRASS.wav" cfxPlayerScore.scoreSound = "Quest Snare 3.wav" @@ -15,6 +15,7 @@ cfxPlayerScore.firstSave = true -- to force overwrite 3.0.2 - interface with ObjectDestructDetector for scoring scenery objects 3.1.0 - shared data for persistence 3.2.0 - integration with bank + 3.3.0 - case INsensitivity for all typeScore objects --]]-- cfxPlayerScore.requiredLibs = { @@ -34,7 +35,7 @@ cfxPlayerScore.killZones = {} -- when set, kills only count here -- typeScore: dictionary sorted by typeString for score -- extend to add more types. It is used by unitType2score to -- determine the base unit score -cfxPlayerScore.typeScore = {} +cfxPlayerScore.typeScore = {} -- ALL UPPERCASE NOW!!! cfxPlayerScore.lastPlayerLanding = {} -- timestamp, by player name cfxPlayerScore.delayBetweenLandings = 30 -- seconds to count as separate landings, also set during take-off to prevent janky t/o to count. cfxPlayerScore.aircraft = 50 @@ -214,9 +215,14 @@ function cfxPlayerScore.object2score(inVictim, killSide) -- does not have group if not inVictim then return 0 end if not killSide then killSide = -1 end local inName = inVictim:getName() + if cfxPlayerScore.verbose then + trigger.action.outText("+++PScr: ob2sc entry to resolve name <" .. inName .. ">", 30) + end if dcsCommon.isSceneryObject(inVictim) then local desc = inVictim:getDesc() - if not desc then return 0 end + if not desc then + return 0 + end -- same as object destruct detector to -- avoid ID changes inName = desc.typeName @@ -231,14 +237,19 @@ function cfxPlayerScore.object2score(inVictim, killSide) -- does not have group if type(inName) == "number" then inName = tostring(inName) end - + if cfxPlayerScore.verbose then + trigger.action.outText("+++PScr: stage II inName: <" .. inName .. ">", 30) + end + -- since 2.7x DCS turns units into static objects for -- cooking off, so first thing we need to do is do a name check - local objectScore = cfxPlayerScore.typeScore[inName] + local objectScore = cfxPlayerScore.typeScore[inName:upper()] if not objectScore then -- try the type desc local theType = inVictim:getTypeName() - objectScore = cfxPlayerScore.typeScore[theType] + if theType then + objectScore = cfxPlayerScore.typeScore[theType:upper()] + end end if type(objectScore) == "string" then @@ -272,17 +283,17 @@ function cfxPlayerScore.unit2score(inUnit) -- simply extend by adding items to the typescore table.concat -- we first try by unit name. This allows individual -- named hi-value targets to have individual scores - local uScore = cfxPlayerScore.typeScore[vicName] + local uScore = cfxPlayerScore.typeScore[vicName:upper()] -- see if all members of group score - if not uScore then + if (not uScore) and vicGroup then local grpName = vicGroup:getName() - uScore = cfxPlayerScore.typeScore[grpName] + uScore = cfxPlayerScore.typeScore[grpName:upper()] end - if uScore == nil then + if not uScore then -- WE NOW TRY TO ACCESS BY VICTIM'S TYPE STRING - uScore = cfxPlayerScore.typeScore[vicType] + uScore = cfxPlayerScore.typeScore[vicType:upper()] else end @@ -291,7 +302,7 @@ function cfxPlayerScore.unit2score(inUnit) uScore = tonumber(uScore) end - if uScore == nil then uScore = 0 end + if not uScore then uScore = 0 end if uScore > 0 then return uScore end -- only apply base scores when the lookup did not give a result @@ -546,13 +557,13 @@ function cfxPlayerScore.isNamedUnit(theUnit) local theName = "(cfx_none)" if type(theUnit) == "string" then theName = theUnit -- direct name assignment - -- WARNING: NO EXIST CHECK DONE! else + -- WARNING: NO EXIST CHECK DONE! -- after kill, unit is dead, so will no longer exist! theName = theUnit:getName() if not theName then return false end end - if cfxPlayerScore.typeScore[theName] then + if cfxPlayerScore.typeScore[theName:upper()] then return true end return false @@ -743,6 +754,9 @@ function cfxPlayerScore.killDetected(theEvent) -- we are only getting called when and if -- a kill occured and killer was a player -- and target exists + if cfxPlayerScore.verbose then + trigger.action.outText("+++PScr: enter kill detected", 30) + end local killer = theEvent.initiator local killerName = killer:getPlayerName() if not killerName then killerName = "" end @@ -757,6 +771,9 @@ function cfxPlayerScore.killDetected(theEvent) -- was it a scenery object? local wasBuilding = dcsCommon.isSceneryObject(victim) if wasBuilding then + if cfxPlayerScore.verbose then + trigger.action.outText("+++PScr: killed objectz was a map/scenery object", 30) + end -- these objects have no coalition; we simply award the score if -- it exists in look-up table. local staticScore = cfxPlayerScore.object2score(victim, killSide) @@ -1431,8 +1448,19 @@ function cfxPlayerScore.start() -- identify and process a score table zones local theZone = cfxZones.getZoneByName("playerScoreTable") if theZone then +-- trigger.action.outText("Reading custom player score table", 30) -- read all into my types registry, replacing whatever is there - cfxPlayerScore.typeScore = cfxZones.getAllZoneProperties(theZone) + cfxPlayerScore.typeScore = theZone:getAllZoneProperties(true) -- true = get all properties in UPPER case +-- local n = dcsCommon.getSizeOfTable(cfxPlayerScore.typeScore) +-- trigger.action.outText("Table has <" .. n .. "> entries:", 30) + if true then + --trigger.action.outText("Custom PlayerScore Type Score Table:", 30) + for name, val in pairs (cfxPlayerScore.typeScore) do +-- trigger.action.outText("ps[" .. name .. "]=<" .. val .. ">", 30) + end + end + else + --trigger.action.outText("No custom score defined", 30) end -- read score tiggers and values diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 85a1170..40b054c 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -1,5 +1,5 @@ radioMenu = {} -radioMenu.version = "2.3.0" +radioMenu.version = "3.0.0" radioMenu.verbose = false radioMenu.ups = 1 radioMenu.requiredLibs = { @@ -7,6 +7,7 @@ radioMenu.requiredLibs = { "cfxZones", -- Zones, of course } radioMenu.menus = {} +radioMenu.mainMenus = {} -- dict --[[-- Version History @@ -21,12 +22,19 @@ radioMenu.menus = {} 2.2.1 - corrected ackD 2.3.0 - added wildcard "*" ability for group name match - added ackASnd .. ackDSnd sounds as options + 3.0.0 - new radioMainMenu and attachTo: mechanics + cascading radioMainMenu support + detect cyclic references --]]-- function radioMenu.addRadioMenu(theZone) table.insert(radioMenu.menus, theZone) end +function radioMenu.addRadioMainMenu(theZone) + radioMenu.mainMenus[theZone.name] = theZone +end + function radioMenu.getRadioMenuByName(aName) for idx, aZone in pairs(radioMenu.menus) do if aName == aZone.name then return aZone end @@ -38,6 +46,10 @@ function radioMenu.getRadioMenuByName(aName) return nil end +function radioMenu.getRadioMainMenuByName(theName) + return radioMenu.mainMenus[theName] +end + -- -- read zone -- @@ -169,6 +181,9 @@ function radioMenu.installMenu(theZone) end theZone.rootMenu = {} + theZone.mainRoot = nil -- can be altered with attachTo + -- see if this menu has an attachTo attribute + theZone.mcdA = {} theZone.mcdB = {} theZone.mcdC = {} @@ -179,8 +194,12 @@ function radioMenu.installMenu(theZone) theZone.mcdD[0] = 0 if theZone.menuGroup or theZone.menuTypes then - for idx, grp in pairs(gID) do - local aRoot = missionCommands.addSubMenuForGroup(grp, theZone.rootName, nil) + for idx, grp in pairs(gID) do + if theZone.attachTo then + local mainMenu = theZone.attachTo + theZone.mainRoot = radioMenu.getMainMenuFor(mainMenu, theZone, grp) + end + local aRoot = missionCommands.addSubMenuForGroup(grp, theZone.rootName, theZone.mainRoot) theZone.rootMenu[grp] = aRoot theZone.mcdA[grp] = 0 theZone.mcdB[grp] = 0 @@ -188,9 +207,17 @@ function radioMenu.installMenu(theZone) theZone.mcdD[grp] = 0 end elseif theZone.coalition == 0 then - theZone.rootMenu[0] = missionCommands.addSubMenu(theZone.rootName, nil) + if theZone.attachTo then + local mainMenu = theZone.attachTo + theZone.mainRoot = radioMenu.getMainMenuFor(mainMenu, theZone, 0) + end + theZone.rootMenu[0] = missionCommands.addSubMenu(theZone.rootName, theZone.mainRoot) else - theZone.rootMenu[0] = missionCommands.addSubMenuForCoalition(theZone.coalition, theZone.rootName, nil) + if theZone.attachTo then + local mainMenu = theZone.attachTo + theZone.mainRoot = radioMenu.getMainMenuFor(mainMenu, theZone, 0) + end + theZone.rootMenu[0] = missionCommands.addSubMenuForCoalition(theZone.coalition, theZone.rootName, theZone.mainRoot) end if theZone:hasProperty("itemA") then @@ -253,6 +280,18 @@ end function radioMenu.createRadioMenuWithZone(theZone) theZone.rootName = theZone:getStringFromZoneProperty("radioMenu", "") + if theZone:hasProperty("attachTo:") then + local attachTo = theZone:getStringFromZoneProperty("attachTo:", "") + if radioMenu.verbose or theZone.verbose then + trigger.action.outText("Menu <" .. theZone.name .. "> will attach to <" .. attachTo .. ">", 30) + end + if not radioMenu.mainMenus[attachTo] then + trigger.action.outText("+++rdoM: menu <" .. theZone.name .. "> tries to attachTo unknown radioMainMenu <" .. attachTo .. "> - cancelled.", 30) + else + theZone.attachTo = radioMenu.mainMenus[attachTo] + end + end + theZone.coalition = theZone:getCoalitionFromZoneProperty("coalition", 0) -- groups / types if theZone:hasProperty("group") then @@ -350,6 +389,116 @@ function radioMenu.createRadioMenuWithZone(theZone) end end +function radioMenu.getMainMenuFor(mainMenu, theZone, idx) + if not idx then idx = 0 end + if not mainMenu.rootMenu[idx] then +-- trigger.action.outText("main <" .. mainMenu.name .. "> for zone <" .. theZone.name .. ">: forcing idx to 0", 30) + return mainMenu.rootMenu[0] + end +-- trigger.action.outText("good main <" .. mainMenu.name .. "> for zone <" .. theZone.name .. ">", 30) + return mainMenu.rootMenu[idx] +end + +function radioMenu.installMainMenu(theZone) + local gID = nil -- set of all groups this menu applies to + if theZone.menuGroup then + if not cfxMX then + trigger.action.outText("WARNING: radioMenu's group attribute requires the 'cfxMX' module", 30) + return + end + -- access cfxMX player info for group ID + gID = radioMenu.filterPlayerIDForGroup(theZone) + elseif theZone.menuTypes then + if not cfxMX then + trigger.action.outText("WARNING: radioMenu's type attribute requires the 'cfxMX' module", 30) + return + end + -- access cxfMX player infor with type match for ID + gID = radioMenu.filterPlayerIDForType(theZone) + end + + theZone.rootMenu = {} -- roots by many different things + local mainRoot = nil + + if theZone.menuGroup or theZone.menuTypes then + for idx, grp in pairs(gID) do + if theZone.attachTo then + local mainMenu = theZone.attachTo + mainRoot = radioMenu.getMainMenuFor(mainMenu, theZone, grp) + end + local aRoot = missionCommands.addSubMenuForGroup(grp, theZone.rootName, mainRoot) + theZone.rootMenu[grp] = aRoot + end + elseif theZone.coalition == 0 then + if theZone.attachTo then + local mainMenu = theZone.attachTo + mainRoot = radioMenu.getMainMenuFor(mainMenu, theZone, grp) + end + theZone.rootMenu[0] = missionCommands.addSubMenu(theZone.rootName, mainRoot) + else + if theZone.attachTo then + local mainMenu = theZone.attachTo + mainRoot = radioMenu.getMainMenuFor(mainMenu, theZone, grp) + end + theZone.rootMenu[0] = missionCommands.addSubMenuForCoalition(theZone.coalition, theZone.rootName, mainRoot) + end + +end + +function radioMenu.createRadioMainMenuWithZone(theZone) + theZone.rootName = theZone:getStringFromZoneProperty("radioMainMenu", "") + + if theZone:hasProperty("radioMenu") then + trigger.action.outText("+++radM: ERROR: main menu <" .. theZone.name .. "> also has conflicting 'radioMenu' entry", 30) + end + + -- CASCADING SUPPORT. LOOP DETECTION + if theZone:hasProperty("attachTo:") then + local attachTo = theZone:getStringFromZoneProperty("attachTo:", "") + if radioMenu.verbose or theZone.verbose then + trigger.action.outText("MAIN Menu <" .. theZone.name .. "> wants to attach to <" .. attachTo .. ">", 30) + end + if not radioMenu.mainMenus[attachTo] then + trigger.action.outText("+++radioMM: MAIN menu <" .. theZone.name .. "> tries to 'attachTo:' unknown radioMainMenu <" .. attachTo .. "> - cancelled.", 30) + else + -- make sure that this zone has been processed + local super = radioMenu.mainMenus[attachTo] + if super.mainDone then + theZone.attachTo = super + else + -- we need other zones to be processed before + if radioMenu.verbose or theZone.verbose then + trigger.action.outText("Main menu <" .. theZone.name .. "> refers to unprocessed zone <" .. attachTo .. ">, deferring.", 30) + end + return false + end + end + end + + -- we now create the ROOT menu that all other menu + -- items attach to that have this as main menu + theZone.coalition = theZone:getCoalitionFromZoneProperty("coalition", 0) + -- groups / types + if theZone:hasProperty("group") then + theZone.menuGroup = theZone:getStringFromZoneProperty("group", "") + theZone.menuGroup = dcsCommon.trim(theZone.menuGroup) + elseif theZone:hasProperty("groups") then + theZone.menuGroup = theZone:getStringFromZoneProperty("groups", "") + theZone.menuGroup = dcsCommon.trim(theZone.menuGroup) + elseif theZone:hasProperty("type") then + theZone.menuTypes = theZone:getStringFromZoneProperty("type", "none") + elseif theZone:hasProperty("types") then + theZone.menuTypes = theZone:getStringFromZoneProperty("types", "none") + end + + -- always install this one + radioMenu.installMainMenu(theZone) + theZone.mainDone = true + if radioMenu.verbose or theZone.verbose then + trigger.action.outText("Main menu <" .. theZone.name .. "> processed", 30) + end + return true +end -- -- Output processing @@ -546,17 +695,9 @@ end function radioMenu.readConfigZone() local theZone = cfxZones.getZoneByName("radioMenuConfig") if not theZone then - if radioMenu.verbose then - trigger.action.outText("+++radioMenu: NO config zone!", 30) - end theZone = cfxZones.createSimpleZone("radioMenuConfig") end - radioMenu.verbose = theZone:getBoolFromZoneProperty("verbose", false) - - if radioMenu.verbose then - trigger.action.outText("+++radioMenu: read config", 30) - end end function radioMenu.start() @@ -572,10 +713,45 @@ function radioMenu.start() -- read config radioMenu.readConfigZone() + -- process radioMainMenu top-level zones + --local filtered = {} + local tries = 0 + local attrZones = cfxZones.getZonesWithAttributeNamed("radioMainMenu") + -- set up all zones so they can 'reach up' even if not yet procced + for k, aZone in pairs (attrZones) do + radioMenu.addRadioMainMenu(aZone) + end + + -- now process, and detect/break cyclic references + repeat + local filtered = {} + for k, aZone in pairs(attrZones) do + radioMenu.createRadioMainMenuWithZone(aZone) -- process attributes + if aZone.mainDone then + -- all good + else + -- wait for next round + table.insert(filtered, aZone) + end + end + done = dcsCommon.getSizeOfTable(filtered) < 1 + tries = tries + 1 + attrZones = filtered + until done or tries > 20 + if tries > 20 then + local msg = "+++radioMenu: ERROR: Cyclic references in menu structure, can't fully process main menus. Unresolved main menus are:\n " + local c = 0 + for idx, theZone in pairs(attrZones) do + if c > 0 then msg = msg .. ", " else c = 1 end + msg = msg .. theZone.name + end + trigger.action.outText(msg .. ".", 30) + end + -- process radioMenu Zones -- old style - local attrZones = cfxZones.getZonesWithAttributeNamed("radioMenu") - for k, aZone in pairs(attrZones) do + local rmZones = cfxZones.getZonesWithAttributeNamed("radioMenu") + for k, aZone in pairs(rmZones) do radioMenu.createRadioMenuWithZone(aZone) -- process attributes radioMenu.addRadioMenu(aZone) -- add to list end @@ -595,4 +771,8 @@ end --[[-- check CD/standby code for multiple groups + + add/remove for mainmenu + visible/invisible for main menus + --]]-- \ No newline at end of file diff --git a/modules/reaper.lua b/modules/reaper.lua new file mode 100644 index 0000000..fc4157b --- /dev/null +++ b/modules/reaper.lua @@ -0,0 +1,708 @@ +reaper = {} +reaper.version = "1.0.0" +reaper.requiredLibs = { + "dcsCommon", + "cfxZones", +} +--[[-- +VERSION HISTORY + + 1.0.0 - Initial Version + + +--]]-- + +reaper.zones = {}-- all zones +reaper.scanning = {} -- zones that are scanning (looking for tgt). by zone name +reaper.tracking = {} -- zones that are tracking tgt. by zone name +reaper.scanInterval = 10 -- seconds +reaper.trackInterval = 0.3 -- seconds + +-- reading reaper zones +function reaper.readReaperZone(theZone) + theZone.myType = string.lower(theZone:getStringFromZoneProperty("reaper", "reaper")) + if dcsCommon.stringStartsWith(theZone.myType, "pre") then theZone.myType = "RQ-1A Predator" else theZone.myType = "MQ-9 Reaper" end + if theZone.myType == "MQ-9 Reaper" then + theZone.alt = 9500 + else theZone.alt = 7500 end theZone.alt = theZone:getNumberFromZoneProperty("alt", theZone.alt) + theZone.coa = theZone:getCoalitionFromZoneProperty("coalition", 2) + if theZone.coa == 0 then + trigger.action.outText("+++Reap: Zone <" .. theZone.name .. "> is of coalition NEUTRAL. Switched to BLUE", 30) + theZone.coa = 2 + end + theZone.enemy = dcsCommon.getEnemyCoalitionFor(theZone.coa) + theZone.onStart = theZone:getBoolFromZoneProperty("onStart", true) + theZone.code = theZone:getNumberFromZoneProperty("code", 1688) + theZone.theTarget = nil + theZone.theSpot = nil + theZone.theUav = nil + theZone.theGroup = nil + theZone.doSmoke = theZone:getBoolFromZoneProperty("doSmoke", false) + theZone.smokeColor = theZone:getSmokeColorStringFromZoneProperty("smokeColor", "red") + theZone.smokeColor = dcsCommon.smokeColor2Num(theZone.smokeColor) + theZone.cost = theZone:getNumberFromZoneProperty("cost", 700) -- for bank integration + theZone.autoRespawn = theZone:getBoolFromZoneProperty("autoRespawn", false) + theZone.launchUI = theZone:getBoolFromZoneProperty("launchUI", true) + theZone.statusUI = theZone:getBoolFromZoneProperty("statusUI", true) + if theZone:hasProperty("launch?") then + theZone.launch = theZone:getStringFromZoneProperty("launch?", "") + theZone.launchVal = theZone:getFlagValue(theZone.launch) + end + if theZone:hasProperty("status?") then + theZone.status = theZone:getStringFromZoneProperty("status?", "") + theZone.statusVal = theZone:getFlagValue(theZone.status) + end + theZone.hasSpawned = false + + if theZone.onStart then + reaper.spawnForZone(theZone) + end +end + +-- spawn a drone from a zone +function reaper.spawnForZone(theZone, ack) + -- create spawn data + local gdata = dcsCommon.createEmptyGroundGroupData (dcsCommon.uuid(theZone.name)) + gdata.task = "Reconnaissance" + gdata.route = {} + -- calculate left and right + local p = theZone:getPoint() + local left, right + -- use dml zone bounds to get upper left and lower right + if theZone.isPoly then + left = dcsCommon.clone(theZone.bounds.ll) -- tried ll + right = dcsCommon.clone(theZone.bounds.ur) -- + else + left = dcsCommon.clone(theZone.bounds.ul) + right = dcsCommon.clone(theZone.bounds.lr) + end + gdata.x = left.x + gdata.y = left.z + + -- build the unit data + local unit = {} + unit.name = dcsCommon.uuid(theZone.name) + unit.x = left.x + unit.y = left.z + unit.type = theZone.myType + unit.skill = "High" + if theZone.myType == "MQ-9 Reaper" then + unit.speed = 55 +-- unit.alt = 9500 + else +-- unit.alt = 7500 + unit.speed = 33 + end + unit.alt = theZone.alt + + -- add to group + gdata.units[1] = unit + + -- now create and add waypoints to route + gdata.route.points = {} + local wp1 = reaper.createInitialWP(left, unit.alt, unit.speed) + gdata.route.points[1] = wp1 + local wp2 = dcsCommon.createSimpleRoutePointData(right, unit.alt, unit.speed) + gdata.route.points[2] = wp2 + + -- spawn the group + local cty = dcsCommon.getACountryForCoalition(theZone.coa) + local theGroup = coalition.addGroup(cty, 0, gdata) + if not theGroup then + trigger.action.outText("+++Reap: failed to spawn for zone <" .. theZone.name .. ">", 30) + return + end + + if theZone.verbose or reaper.verbose then + trigger.action.outText("+++reap: Spawned <" .. theGroup:getName() .. "> reaper", 30) + end + if ack then + trigger.action.outTextForCoalition(theZone.coa, "Drone <" .. theZone.name .. "> on station", 30) + trigger.action.outSoundForCoalition(theZone.coa, reaper.actionSound) + end + + reaper.cleanUp(theZone) -- dealloc anything still there + + theZone.theGroup = theGroup + local uavs = theGroup:getUnits() + theZone.theUav = uavs[1] + theZone.theTarget = nil + theZone.theSpot = nil + theZone.hasSpawned = true + reaper.scanning[theZone.name] = theZone +end + +function reaper.cleanUp(theZone) + if theZone.theUav and Unit.isExist(theZone.theUav) then + Unit.destroy(theZone.theUav) + end + theZone.theUav = nil + theZone.theTarget = nil + theZone.theGroup = nil + if theZone.theSpot then + Spot.destroy(theZone.theSpot) + end + theZone.theSpot = nil +end + +function reaper.createInitialWP(p, alt, speed) + local wp = { + ["alt"] = alt, + ["action"] = "Turning Point", + ["alt_type"] = "BARO", + ["properties"] = { + ["addopt"] = {}, -- end of ["addopt"] + }, -- end of ["properties"] + ["speed"] = speed, + ["task"] = { + ["id"] = "ComboTask", + ["params"] = { + ["tasks"] = { + [1] = { + ["enabled"] = true, + ["auto"] = true, + ["id"] = "WrappedAction", + ["number"] = 1, + ["params"] = { + ["action"] = { + ["id"] = "EPLRS", + ["params"] = { + ["value"] = true, + ["groupId"] = 1, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [1] + [2] = { + ["enabled"] = true, + ["auto"] = false, + ["id"] = "Orbit", + ["number"] = 2, + ["params"] = { + ["altitude"] = alt, + ["pattern"] = "Race-Track", + ["speed"] = speed, + }, -- end of ["params"] + }, -- end of [2] + }, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["type"] = "Turning Point", + ["ETA"] = 0, + ["ETA_locked"] = true, + ["y"] = p.z, + ["x"] = p.x, + ["speed_locked"] = true, + ["formation_template"] = "", + } -- end of wp + return wp +end + +-- scanning & tracking +-- scanning looks for vehicles to track, and exectues much less often +-- tracking tracks a single vehicle and places a pointer on it +function reaper.findFirstEnemyUnitVisible(enemies, theZone) + local p = theZone.theUav:getPoint() + -- we assume a flat altitude of 7000m + local visRange = theZone.alt * 1 -- based on tan(45) = 1 --> range = alt + for idx, aGroup in pairs(enemies) do + local theUnits = aGroup:getUnits() + -- optimization: only scan the first vehicle in group if it's in range local theUnit = theUnits[1] + local theUnit = theUnits[1] + if theUnit and Unit.isExist(theUnit) then + up = theUnit:getPoint() + d = dcsCommon.distFlat(up, p) + if d < visRange then + -- try each unit if it is visible from drone + for idy, aUnit in pairs(theUnits) do + local up = aUnit:getPoint() + up.y = up.y + 2 + if land.isVisible(p, up) then return aUnit end + end + end + end + end +end + +function reaper.scan() + -- how far can the drone see? we calculate with a 120 degree opening + -- camera lens, making half angle = 45 --> tan(45) = 1 + -- so the radius of the visible circle on the ground is 1 * altidude + timer.scheduleFunction(reaper.scan, {}, timer.getTime() + reaper.scanInterval) + filtered = {} + local redEnemies = coalition.getGroups(2, 2) -- blue ground vehicles + local blueEnemeis = coalition.getGroups(1, 2) -- get ground vehicles + for name, theZone in pairs(reaper.scanning) do + local enemies = redEnemies + if theZone.coa == 2 then enemies = blueEnemeis end + if Unit.isExist(theZone.theUav) then + local theTarget = reaper.findFirstEnemyUnitVisible(enemies, theZone) + if theTarget then + -- add a laser tracker to this unit + local lp = theTarget:getPoint() + local lat, lon, alt = coord.LOtoLL(lp) + lat, lon = dcsCommon.latLon2Text(lat, lon) + + local theSpot = Spot.createLaser(theZone.theUav, {0, 2, 0}, lp, theZone.code) + if theZone.doSmoke then + trigger.action.smoke(lp , theZone.smokeColor ) + end + trigger.action.outTextForCoalition(theZone.coa, "Drone <" .. theZone.name .. "> is tracking a <" .. theTarget:getTypeName() .. "> at " .. lat .. " " .. lon .. ", code " .. theZone.code, 30) + trigger.action.outSoundForCoalition(theZone.coa, reaper.actionSound) + theZone.theTarget = theTarget + if theZone.theSpot then + theZone.theSpot:destroy() + end + theZone.theSpot = theSpot + -- put me in track mode + reaper.tracking[name] = theZone + else + -- will scan again + filtered[name] = theZone + end + else + -- does not remain + if theZone.verbose or reaper.verbose then + trigger.action.outText("+++reap: drone from <" .. theZone.name .. "> no longer exists", 30) + end + trigger.action.outTextForCoalition(theZone.coa, "Drone <" .. theZone.name .. "> lost.", 30) + trigger.action.outSoundForCoalition(theZone.coa, reaper.actionSound) + theZone.theUav = nil + theZone.theSpot = nil + theZone.theTarget = nil + theZone.theGroup = nil + end + end + reaper.scanning = filtered +end + +function reaper.track() + local filtered = {} + for name, theZone in pairs(reaper.tracking) do + -- check if uav still alive + if Unit.isExist(theZone.theUav) then + if Unit.isExist(theZone.theTarget) then + -- update stop + local d = theZone.theTarget:getPoint() + theZone.theSpot:setPoint(d) + filtered[name] = theZone + else + trigger.action.outTextForCoalition(theZone.coa, "Drone <" .. theZone.name .. "> searching for new targets", 30) + trigger.action.outSoundForCoalition(theZone.coa, reaper.actionSound) + if theZone.theSpot then + theZone.theSpot:destroy() + end + theZone.theSpot = nil + reaper.scanning[name] = theZone -- back to scanning + end + else + trigger.action.outTextForCoalition(theZone.coa, "Drone <" .. theZone.name .. "> lost", 30) + trigger.action.outSoundForCoalition(theZone.coa, reaper.actionSound) + if theZone.theSpot then + theZone.theSpot:destroy() + end + theZone.theSpot = nil + theZone.theUav = nil + theZone.theGroup = nil + end + end + reaper.tracking = filtered + timer.scheduleFunction(reaper.track, {}, timer.getTime() + reaper.trackInterval) +end + +function reaper.update() + timer.scheduleFunction(reaper.update, {}, timer.getTime() + 1) + + -- go through all my zones, and respawn those that have no + -- uav but have autoRespawn active + + for name, theZone in pairs(reaper.zones) do + if theZone.autoRespawn and not theZone.theUav and theZone.hasSpawned then + -- auto-respawn needs to kick in + reaper.scanning[name] = nil + reaper.tracking[name] = nil + if reaper.verbose or theZone.verbose then + trigger.action.outText("+++reap: respawning for <" .. name .. ">", 30) + end + reaper.spawnForZone(theZone) + end + + if theZone.status and theZone:testZoneFlag(theZone.status, "change", "statusVal") then + if theZone.verbose then + trigger.action.outText("+++reap: Triggered status for zone <" .. name .. "> on <" .. theZone.status .. ">", 30) + end + reaper.doSingleDroneStatus(theZone) + end + + if theZone.launch and theZone:testZoneFlag(theZone.launch, "change", "launchVal") then + args = {} + args[1] = theZone.coa -- = args[1] + args[2] = name -- = args[2] + reaper.doLaunch(args) + end + end + + -- now poll my (global) status flags + if reaper.blueStatus and cfxZones.testZoneFlag(reaper, reaper.blueStatus, "change", "blueStatusVal") then + reaper.doDroneStatusBlue() + end + if reaper.redStatus and cfxZones.testZoneFlag(reaper, reaper.redStatus, "change", "redStatusVal") then + reaper.doDroneStatusRed() + end +end + +-- +-- UI +-- +function reaper.installFullUIForCoa(coa) + -- install "Drone Control" as root for red and blue + + local mainMenu = nil + if reaper.mainMenu then + mainMenu = radioMenu.getMainMenuFor(reaper.mainMenu) -- nilling both next params will return menus[0] + end + + local root = missionCommands.addSubMenuForCoalition(coa, reaper.menuName, mainMenu) + -- now install submenus + local c1 = missionCommands.addCommandForCoalition(coa, "Drone Status", root, reaper.redirectDroneStatus, {coa,}) + local r2 = missionCommands.addSubMenuForCoalition(coa, "Launch Drones", root) + reaper.installLaunchersForCoa(coa, r2) +end + +function reaper.installLaunchersForCoa(coa, root) + -- WARNING: we currently install commands, may overflow! +-- trigger.action.outText("enter launchers builder", 30) + local filtered = {} + for name, theZone in pairs(reaper.zones) do + if theZone.coa == coa and theZone.launchUI then + filtered[name] = theZone + end + end + local n = dcsCommon.getSizeOfTable(filtered) + if n > 10 then + trigger.action.outText("+++reap: WARNING too many (" .. n .. ") launchers for coa <" .. coa .. ">", 30) + return + end + + for name, theZone in pairs(filtered) do +-- trigger.action.outText("proccing " .. name, 30) + mnu = theZone.name .. ": " .. theZone.myType + if bank and reaper.useCost then + -- requires bank module + mnu = mnu .. "(§" .. theZone.cost .. ")" + end + local args = {coa, name, } + local r3 = missionCommands.addCommandForCoalition(coa, mnu, root, reaper.redirectLaunch, args) + end +end + +function reaper.redirectDroneStatus(args) + timer.scheduleFunction(reaper.doDroneStatus, args, timer.getTime() + 0.1) +end + +function reaper.redirectLaunch(args) + timer.scheduleFunction(reaper.doLaunch, args, timer.getTime() + 0.1) +end + +-- +-- DML API for UI +-- +function reaper.doDroneStatusRed() + reaper.doDroneStatus({1,}) +end + +function reaper.doDroneStatusBlue() + reaper.doDroneStatus({2,}) +end + +function reaper.doDroneStatus(args) + local coa = args[1] +-- trigger.action.outText("enter do drone status for coa " .. coa, 30) + local done = {} + local msg = "" + local filtered = {} + for name, theZone in pairs(reaper.tracking) do + if theZone.coa == coa and theZone.statusUI then + filtered[name] = theZone + end + end + local n = dcsCommon.getSizeOfTable(filtered) + -- collect tracking drones + if n > 0 then + msg = msg .. "\nThe following drones are tracking targets:" + for name, theZone in pairs(filtered) do + msg = msg .. "\n <" .. name .. ">: " + local theTarget = theZone.theTarget + if theTarget and Unit.isExist(theTarget) then + local lp = theTarget:getPoint() + local lat, lon, alt = coord.LOtoLL(lp) + lat, lon = dcsCommon.latLon2Text(lat, lon) + local ut = theTarget:getTypeName() + msg = msg .. ut .. " at " .. lat .. ", " .. lon .. " code " .. theZone.code + else + msg = msg .. "" + end + done[name] = true + end + else + msg = msg .. "\n(No drones are tracking a target)\n" + end + + -- collect loitering drones + filtered = {} + for name, theZone in pairs(reaper.scanning) do + if theZone.coa == coa and theZone.statusUI then filtered[name] = theZone end + end + n = dcsCommon.getSizeOfTable(filtered) + if n > 0 then + msg = msg .. "\n\nThe following drones are loitering on-station" + for name, theZone in pairs(filtered) do + msg = msg .. "\n <" .. name .. ">: (" .. theZone.myType .. ")" + done[name] = true + end + else + msg = msg .. "\n\n(No drones are loitering)\n" + end + + filtered = {} + for name, theZone in pairs(reaper.zones) do + if theZone.coa == coa and theZone.statusUI and not done[name] then + filtered[name] = theZone + end + end + n = dcsCommon.getSizeOfTable(filtered) + if n > 0 then + msg = msg .. "\n\nThe following drones are ready to launch" + for name, theZone in pairs(filtered) do + msg = msg .. "\n <" .. name .. ">: " .. theZone.myType .. " " + if bank and reaper.useCost then + msg = msg .. "(§" .. theZone.cost .. ")" + end + end + msg = msg .. "\n" + else + msg = msg .. "\n\n(All drones have launched)\n" + end + + trigger.action.outTextForCoalition(coa, msg, 30) + trigger.action.outSoundForCoalition(coa, reaper.actionSound) +end + +function reaper.doSingleDroneStatus(theZone) + local coa = theZone.coa +-- trigger.action.outText("enter SINGLE drone status for coa " .. coa, 30) + local msg = "" + local name = theZone.name + -- see if drone is tracking + if reaper.tracking[name] and theZone.theTarget then + msg = "<" .. name .. ">: " + local theTarget = theZone.theTarget + if theTarget and Unit.isExist(theTarget) then + local lp = theTarget:getPoint() + local lat, lon, alt = coord.LOtoLL(lp) + lat, lon = dcsCommon.latLon2Text(lat, lon) + local ut = theTarget:getTypeName() + msg = msg .. ut .. " at " .. lat .. ", " .. lon .. " code " .. theZone.code + else + msg = msg .. "[signal failure, please try later]" + end + trigger.action.outTextForCoalition(coa, msg, 30) + trigger.action.outSoundForCoalition(coa, reaper.actionSound) + return + end + + -- see if drone is loitering + if reaper.scanning[name] then + msg = "<" .. name .. ">: (" .. theZone.myType .. ") loitering, scanning for targets" + trigger.action.outTextForCoalition(coa, msg, 30) + trigger.action.outSoundForCoalition(coa, reaper.actionSound) + return + end + + msg = "<" .. name .. ">: " .. theZone.myType .. " " + if bank and reaper.useCost then + msg = msg .. "(§" .. theZone.cost .. ") " + end + msg = msg .. "ready to launch" + trigger.action.outTextForCoalition(coa, msg, 30) + trigger.action.outSoundForCoalition(coa, reaper.actionSound) +end + +function reaper.doLaunch(args) + coa = args[1] + name = args[2] + -- check if we can launch + local theZone = reaper.zones[name] + if not theZone then + trigger.action.outText("+++reap: something strange happened with launcher <" .. name .. ">", 30) + return + end + + if theZone.theUav and Unit.isExist(theZone.theUav) then + trigger.action.outTextForCoalition(coa, "Drone <" .. name .. "> is already on-station", 30) + trigger.action.outSoundForCoalition(coa, reaper.actionSound) + return + end + local hasBalance, amount = 0, 0 + -- money check if enabled + if bank and reaper.useCost then + hasBalance, amount = bank.getBalance(coa) + if not hasBalance then + amount = 0 + end + + if amount < theZone.cost then + trigger.action.outTextForCoalition(coa, "Insufficient funds (§" .. theZone.cost .. " required, you have §" .. amount, 30) + trigger.action.outSoundForCoalition(coa, reaper.actionSound) + return + end + end + + -- ok, go for launch + reaper.spawnForZone(theZone) + + -- subtract funds + if bank and reaper.useCost then + trigger.action.outTextForCoalition(coa, "Launching <" .. theZone.myType .. "> drone for §" .. theZone.cost .. ", §" .. amount - theZone.cost .. " remaining.", 30) + bank.withdawFunds(coa, theZone.cost) + else + trigger.action.outTextForCoalition(coa, "Launching <" .. theZone.myType .. "> drone.", 30) + end + trigger.action.outSoundForCoalition(coa, reaper.actionSound) +end + +-- +-- config +-- +function reaper.readConfigZone() + local theZone = cfxZones.getZoneByName("reaperConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("reaperConfig") + end + reaper.name = "reaperConfig" -- zones comaptibility + reaper.actionSound = theZone:getStringFromZoneProperty("actionSound", "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") + reaper.UI = theZone:getBoolFromZoneProperty("UI", true) + reaper.menuName = theZone:getStringFromZoneProperty("menuName", "Drone Command") + reaper.useCost = theZone:getBoolFromZoneProperty("useCost", true) + if theZone:hasProperty("blueStatus?") then + reaper.blueStatus = theZone:getStringFromZoneProperty("blueStatus?", "") + reaper.blueStatusVal = theZone:getFlagValue(reaper.blueStatus) -- save last value + end + if theZone:hasProperty("redStatus?") then + reaper.redStatus = theZone:getStringFromZoneProperty("redStatus?", "") + reaper.redStatusVal = theZone:getFlagValue(reaper.redStatus) -- save last value + end + + if theZone:hasProperty("attachTo:") then + local attachTo = theZone:getStringFromZoneProperty("attachTo:", "") + if radioMenu then + local mainMenu = radioMenu.mainMenus[attachTo] + if mainMenu then + reaper.mainMenu = mainMenu + else + trigger.action.outText("+++reaper: cannot find super menu <" .. attachTo .. ">", 30) + end + else + trigger.action.outText("+++reaper: REQUIRES radioMenu to run before reaper. 'AttachTo:' ignored.", 30) + end + end + reaper.verbose = theZone.verbose +end + +-- persistence + function reaper.saveData() + local theData = {} + -- save all non-self-starting, yet running reapers + local running = {} + for name, theZone in pairs (reaper.zones) do + if (not theZone.onStart) and (theZone.theUav) + and Unit.isExist(theZone.theUav) then + running[name] = true + end + end + theData.running = running + + return theData, reaper.sharedData +end + +function reaper.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("reaper", reaper.sharedData) + if not theData then + if reaper.verbose then + trigger.action.outText("+++reaper: no save data received, skipping.", 30) + end + return + end + local running = theData.running + if theData.running then + for name, ignore in pairs (running) do + local theZone = reaper.zones[name] + if theZone then + reaper.spawnForZone(theZone) + else + trigger.action.outText("+++reaper - persistence: zone <" .. name .. "> does not exist", 30) + end + end + end +end + +-- go go go +function reaper.start() + if not dcsCommon.libCheck then + trigger.action.outText("cfx reaper requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx reaper", reaper.requiredLibs) then + return false + end + + -- read config + reaper.readConfigZone() + + -- read reaper zones + local rZones = cfxZones.zonesWithProperty("reaper") + for k, aZone in pairs(rZones) do + reaper.readReaperZone(aZone) + reaper.zones[aZone.name] = aZone + end + + -- install UI if desired + if reaper.UI then + local coas = {1, 2} + for idx, coa in pairs(coas) do + reaper.installFullUIForCoa(coa) + end + end + + -- load data if persisted + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = reaper.saveData + persistence.registerModule("reaper", callbacks) + -- now load my data + reaper.loadData() + end + + -- schedule first update + timer.scheduleFunction(reaper.update, {}, timer.getTime() + 1) + + -- schedule scan and track loops + timer.scheduleFunction(reaper.scan, {}, timer.getTime() + 1) + timer.scheduleFunction(reaper.track, {}, timer.getTime() + 1) + + trigger.action.outText("reaper v " .. reaper.version .. " running.", 30) + return true +end + +if not reaper.start() then + trigger.action.outText("Reaper failed to start", 30) +end + +--[[-- + Idea: mobile launch vehicle, zone follows apc around. Can even be hauled along with hook + idea: prioritizing targets in a group + fix quad zone waypoints + filter targets for lasing by list? +--]]-- diff --git a/modules/scribe.lua b/modules/scribe.lua index 8ae1bfb..5cac9cd 100644 --- a/modules/scribe.lua +++ b/modules/scribe.lua @@ -1,5 +1,5 @@ scribe = {} -scribe.version = "1.1.0" +scribe.version = "2.0.0" scribe.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -12,6 +12,7 @@ VERSION HISTORY 1.0.0 Initial Version 1.0.1 postponed land, postponed takeoff, unit_lost 1.1.0 supports persistence's SHARED ability to share data across missions + 2.0.0 support for main menu --]]-- scribe.verbose = true scribe.db = {} -- indexed by player name @@ -488,6 +489,12 @@ function scribe.startPlayerGUI() -- note: currently assumes single-player groups -- in preparation of single-player 'commandForUnit' -- ASSUMES SINGLE-UNIT PLAYER GROUPS! + local mainMenu = nil + if scribe.mainMenu then + mainMenu = radioMenu.getMainMenuFor(scribe.mainMenu) -- nilling both next params will return menus[0] + end + + for uName, uData in pairs(cfxMX.playerUnitByName) do local unitInfo = {} -- try and access each unit even if we know that the @@ -508,7 +515,7 @@ function scribe.startPlayerGUI() unitInfo.uID = uData.unitId unitInfo.theType = theType unitInfo.cat = cfxMX.groupTypeByName[gName] - unitInfo.root = missionCommands.addSubMenuForGroup(unitInfo.gID, scribe.uiMenu) + unitInfo.root = missionCommands.addSubMenuForGroup(unitInfo.gID, scribe.uiMenu, mainMenu) unitInfo.checkData = missionCommands.addCommandForGroup(unitInfo.gID, "Get Pilot's Statistics", unitInfo.root, scribe.redirectCheckData, unitInfo) end end @@ -524,6 +531,24 @@ function scribe.readConfigZone() scribe.verbose = theZone.verbose scribe.hasGUI = theZone:getBoolFromZoneProperty("hasGUI", true) scribe.uiMenu = theZone:getStringFromZoneProperty("uiMenu", "Mission Logbook") + + scribe.name = "scribeConfig" -- zones comaptibility + + if theZone:hasProperty("attachTo:") then + local attachTo = theZone:getStringFromZoneProperty("attachTo:", "") + if radioMenu then + local mainMenu = radioMenu.mainMenus[attachTo] + if mainMenu then + scribe.mainMenu = mainMenu + else + trigger.action.outText("+++scribe: cannot find super menu <" .. attachTo .. ">", 30) + end + else + trigger.action.outText("+++scribe: REQUIRES radioMenu to run before scribe. 'AttachTo:' ignored.", 30) + end + end + + scribe.greetPlayer = theZone:getBoolFromZoneProperty("greetPlayer", true) scribe.byePlayer = theZone:getBoolFromZoneProperty("byebyePlayer", true) scribe.landings = theZone:getBoolFromZoneProperty("landings", true) diff --git a/modules/unitPersistence.lua b/modules/unitPersistence.lua index 57af492..283eccb 100644 --- a/modules/unitPersistence.lua +++ b/modules/unitPersistence.lua @@ -467,7 +467,7 @@ function unitPersistence.loadMission() end if mismatchWarning then - trigger.action.outText("\n+++WARNING: \nSaved data does not match mission. You should re-start from scratch\n", 30) + trigger.action.outText("\n+++WARNING: \nSaved unit data does not match mission. You should re-start from scratch\n", 30) end -- set mission according to data received from last save if unitPersistence.verbose then diff --git a/modules/williePete.lua b/modules/williePete.lua index 6c5fb51..f3e4b7c 100644 --- a/modules/williePete.lua +++ b/modules/williePete.lua @@ -1,5 +1,5 @@ williePete = {} -williePete.version = "2.0.2" +williePete.version = "2.0.3" williePete.ups = 10 -- we update at 10 fps, so accuracy of a -- missile moving at Mach 2 is within 33 meters, -- with interpolation even at 3 meters @@ -19,6 +19,7 @@ williePete.requiredLibs = { - getFirstLivingPlayerInGroupNamed() 2.0.1 - added Harrier's FFAR M156 WP 2.0.2 - hardened playerUpdate() + 2.0.3 - further hardened playerUpdate() --]]-- williePete.willies = {} @@ -635,7 +636,7 @@ function williePete.playerUpdate() end end else - trigger.action.outText("+++wp: strange issues with group <" .. gName .. ">, does not exist. Skipped in playerUpdate()", 30) + trigger.action.outText("+++wp: strange issues with group <" .. unitInfo.gName .. ">, does not exist. Skipped in playerUpdate()", 30) end if dropUnit then -- all outside, remove from zone check-in diff --git a/modules/wiper.lua b/modules/wiper.lua index 52782e7..ab0c65d 100644 --- a/modules/wiper.lua +++ b/modules/wiper.lua @@ -1,5 +1,5 @@ wiper = {} -wiper.version = "1.2.0" +wiper.version = "1.3.0" wiper.verbose = false wiper.ups = 1 wiper.requiredLibs = { @@ -15,6 +15,9 @@ wiper.wipers = {} - categories can now be a list - declutter opetion - if first category is 'none', zone will not wipe at all but may declutter + 1.3.0 - now warns when wiper zone is polygonal + - enhanced verbosity for dictionary setups + - now warns of possible incompatibility with cloners --]]-- @@ -39,6 +42,11 @@ end function wiper.createWiperWithZone(theZone) theZone.triggerWiperFlag = theZone:getStringFromZoneProperty("wipe?", "*") + -- see if the zone also has a 'cloner' attribute, and warn + if theZone:hasProperty("cloner") then + trigger.action.outText("+++Wpr: Zone <" .. theZone.name .. "> is also a CLONER - check usage of 'wipe?' attribute.", 30) + end + -- triggerWiperMethod theZone.triggerWiperMethod = theZone:getStringFromZoneProperty("triggerMethod", "change") if theZone:hasProperty("triggerWiperMethod") then @@ -98,7 +106,7 @@ function wiper.createWiperWithZone(theZone) end theDict[shortName] = ew if wiper.verbose or theZone.verbose then - trigger.action.outText("+++wpr: dict [".. shortName .."], '*' = " .. dcsCommon.bool2Text(ew) .. " for <" .. theZone:getName() .. ">",30) + trigger.action.outText("+++wpr: dict [".. shortName .."], '*' = " .. dcsCommon.bool2Text(ew) .. " successful for <" .. theZone:getName() .. ">",30) end end theZone.wipeNamed = theDict @@ -106,6 +114,10 @@ function wiper.createWiperWithZone(theZone) theZone.wipeInventory = theZone:getBoolFromZoneProperty("wipeInventory", false) + if theZone.isPoly then + trigger.action.outText("+++wpr: WARNING: wiper zone <" .. theZone.name .. "> is NOT CIRCULAR, but quad-based. Expect erratic behavior!", 30) + end + if wiper.verbose or theZone.verbose then trigger.action.outText("+++wpr: new wiper zone <".. theZone.name ..">", 30) end diff --git a/tutorial & demo missions/demo - What's on the cascading menu.miz b/tutorial & demo missions/demo - What's on the cascading menu.miz new file mode 100644 index 0000000..5ebdc83 Binary files /dev/null and b/tutorial & demo missions/demo - What's on the cascading menu.miz differ diff --git a/tutorial & demo missions/demo - reaper, man.miz b/tutorial & demo missions/demo - reaper, man.miz new file mode 100644 index 0000000..ca87c02 Binary files /dev/null and b/tutorial & demo missions/demo - reaper, man.miz differ