diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 755992f..65eeb5e 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 279ef0c..533c06e 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 992daab..f979f61 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "4.3.0" +cfxZones.version = "4.3.1" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -46,6 +46,8 @@ cfxZones.version = "4.3.0" - 4.1.2 - hash property missing warning - 4.2.0 - new createRandomPointInPopulatedZone() - 4.3.0 - boolean supports maybe, random, rnd, ? + - small optimization for randomInRange() + - randomDelayFromPositiveRange also allows 0 --]]-- @@ -977,7 +979,7 @@ function cfxZones.isGroupPartiallyInZone(aGroup, aZone) if not aGroup:isExist() then return false end local allUnits = aGroup:getUnits() for uk, aUnit in pairs (allUnits) do - if aUnit:isExist() and aUnit:getLife() > 1 then + if Unit.isExist(aUnit) and aUnit:getLife() > 1 then local p = aUnit:getPoint() local inzone, percent, dist = cfxZones.pointInZone(p, aZone) if inzone then @@ -2244,8 +2246,9 @@ end function cfxZones.randomDelayFromPositiveRange(minVal, maxVal) -- should be moved to dcsCommon if not maxVal then return minVal end if not minVal then return maxVal end + if minVal == maxVal then return minVal end local delay = maxVal - if minVal > 0 and minVal < delay then + if minVal >= 0 and minVal < delay then -- we want a randomized from time from minTime .. delay local varPart = delay - minVal + 1 varPart = dcsCommon.smallRandom(varPart) - 1 diff --git a/modules/clients.lua b/modules/clients.lua new file mode 100644 index 0000000..764d186 --- /dev/null +++ b/modules/clients.lua @@ -0,0 +1,199 @@ +clients = {} +clients.version = "0.0.0" +clients.ups = 1 +clients.verbose = false +clients.netlog = true +clients.players = {} +-- player entry: indexed by name +-- playerName - name of player, same as index +-- uName = unit name +-- coa = coalition +-- connected = true/false is currently ingame + +function clients.out(msg) + -- do some preprocessing? + if clients.verbose then + trigger.action.outText(msg, 30) + end + -- add to own log? + if clients.netlog then + env.info(msg) + end +end + +--[[-- +Event ID: + 1 = player enters mission for first time + 2 = player enters unit + 3 = player leaves unit + 4 = player changes unit + 5 = player changes coalition + + +Sequence of events + Player enters mission first time + - player enters mission (new player) ID = 1 + - player enters unit ID = 2 + + Player is no longer active (their unit is gone) + - player leaves unit ID = 3 + + Player enters unit after having already been in the mission + - (player changes coalition) if unit belongs to different coa + - (player changes unit if unit different than before) + - player enters unit + +--]]-- +-- +-- client events +-- +clients.cb = {} -- profile = (id, this, last) +function clients.invokeCallbacks(ID, this, last) + for idx, cb in pairs(clients.cb) do + cb(ID, this, last) + end +end + +function clients.addCallback(theCB) + table.insert(clients.cb, theCB) +end + + +function clients.playerEnteredMission(thisTime) + clients.out("clients: Player <" .. thisTime.playerName .. "> enters mission for the first time") + clients.invokeCallbacks(1, thisTime) +end + +function clients.playerEnteredUnit(thisTime) + -- called when player enters a unit + clients.out("clients: Player <" .. thisTime.playerName .. "> enters Unit <" .. thisTime.uName .. ">.") + clients.invokeCallbacks(2, thisTime) +end + +function clients.playerLeavesUnit(lastTime) + -- called when player leaves a unit + clients.out("clients: Player <" .. lastTime.playerName .. "> leaves Unit <" .. lastTime.uName .. ">.") + clients.invokeCallbacks(3, lastTime) +end + +function clients.playerChangedUnits(thisTime, lastTime) + -- called when player enters a different unit + clients.out("clients: Player <" .. thisTime.playerName .. "> changes from Unit <" .. lastTime.uName .. "> to NEW unit <" .. thisTime.uName .. ">.") + clients.invokeCallbacks(4, thisTime, lastTime) +end + +function clients.playerChangedCoalition(thisTime, lastTime) + -- called when player enters a different unit + clients.out("clients: Player <" .. thisTime.playerName .. "> changes from coalition <" .. lastTime.coa .. "> to NEW coalition <" .. thisTime.coa .. ">.") + clients.invokeCallbacks(4, thisTime, lastTime) +end + +-- check all connected player units +function clients.compareStatus(thisTime, lastTime) + if lastTime then + -- they were known last time I checked. see if they were in-game + if thisTime.connected == lastTime.connected then + -- status is the same as before + else + -- player entered or left mission, and was known last time + if thisTime.connected then + -- player connected but was known, do nothing + else + -- player left mission. do we want to record this? + end + end + -- check if they have the same unit name + -- if not, check if they have changed coas + if lastTime.uName == thisTime.uName then + -- same unit, all is fine + else + -- new unit. check if same side + if lastTime.coa == thisTime.coa then + -- player stayed in same coa + else + -- player changed coalition + clients.playerChangedCoalition(thisTime, lastTime) + end + clients.playerEnteredUnit(thisTime) + clients.playerChangedUnits(thisTime, lastTime) + end + else + -- player is new to mission + clients.playerEnteredMission(thisTime) + clients.playerEnteredUnit(thisTime) + end +end + +function clients.checkPlayers() + local connectedNow = {} -- players that are connected now + local allCoas = {0, 1, 2} + -- collect all currently connected players + for idx, coa in pairs(allCoas) do + local cPlayers = coalition.getPlayers(coa) -- gets UNITS! + for idy, aPlayerUnit in pairs(cPlayers) do + if aPlayerUnit and Unit.isExist(aPlayerUnit) then + local entry = {} + local playerName = aPlayerUnit:getPlayerName() + entry.playerName = playerName + entry.uName = aPlayerUnit:getName() + entry.coa = coa + entry.connected = true + connectedNow[playerName] = entry + + -- see if they were connected last time we checked + local lastTime = clients.players[playerName] + clients.compareStatus(entry, lastTime) + end + end + end + + -- now find players who are no longer represented and + -- event them + for aPlayerName, lastTime in pairs(clients.players) do + local thisTime = connectedNow[aPlayerName] + if thisTime then + -- is also present now. skip + else + -- no longer active, see if they were active last time + if lastTime.connected then + -- they were active, generate disco event + clients.playerLeavesUnit(lastTime) + end + lastTime.connected = false + -- keep on roster + connectedNow[aPlayerName] = lastTime + end + end + + clients.players = connectedNow +end + +function clients.update() + timer.scheduleFunction(clients.update, {}, timer.getTime() + 1) + clients.checkPlayers() +end + +-- +-- Event handling +-- +function clients:onEvent(theEvent) + if not theEvent then return end + local theUnit = theEvent.initiator + if not theUnit then return end + if not theUnit.getPlayerName or not theUnit:getPlayerName() then return end + + -- we have a player birth. Simply invoke checkplayers + clients.out("clients: detected player birth event.") + clients.checkPlayers() +end + +-- +-- Start +-- +function clients.start() + world.addEventHandler(clients) + timer.scheduleFunction(clients.update, {}, timer.getTime() + 1) + trigger.action.outText("clients v" .. clients.version .. " running.", 30) +end + +clients.start() \ No newline at end of file diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index 0a973d4..d219342 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -98,8 +98,6 @@ function cloneZones.partOfGroupDataInZone(theZone, theUnits) uP.x = aUnit.x uP.y = 0 uP.z = aUnit.y -- !! y-z - --local dist = dcsCommon.dist(uP, zP) - --if dist <= theZone.radius then return true end if theZone:pointInZone(uP) then return true end end return false @@ -107,7 +105,6 @@ end function cloneZones.allGroupsInZoneByData(theZone) local theGroupsInZone = {} - local radius = theZone.radius for groupName, groupData in pairs(cfxMX.groupDataByName) do if groupData.units then if cloneZones.partOfGroupDataInZone(theZone, groupData.units) then diff --git a/modules/commander.lua b/modules/commander.lua index d7c8f33..f36c254 100644 --- a/modules/commander.lua +++ b/modules/commander.lua @@ -4,7 +4,7 @@ -- *** EXTENDS ZONES: 'pathing' attribute -- cfxCommander = {} -cfxCommander.version = "1.1.3" +cfxCommander.version = "1.1.4" --[[-- VERSION HISTORY - 1.0.5 - createWPListForGroupToPointViaRoads: detect no road found - 1.0.6 - build in more group checks in assign wp list @@ -29,6 +29,7 @@ cfxCommander.version = "1.1.3" - added delay defaulting for most scheduling functions - 1.1.3 - isExist() guard improvements for multiple methods - cleaned up comments + - 1.1.4 - hardened makeGroupGoThere() --]]-- @@ -337,6 +338,18 @@ function cfxCommander.makeGroupGoThere(group, there, speed, formation, delay) if type(group) == 'string' then -- group name group = Group.getByName(group) end + + if not Group.isExist(group) then + trigger.action.outText("cmdr: makeGroupGoThere() - group does not exist", 30) + return + end + + -- check that we can get a location for the group + local here = dcsCommon.getGroupLocation(group) + if not here then + return + end + local wp = cfxCommander.createWPListForGroupToPoint(group, there, speed, formation) cfxCommander.assignWPListToGroup(group, wp, delay) diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index c950f88..a747fbb 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -40,6 +40,8 @@ csarManager.ups = 1 3.2.4 - pass theZone with missionCreateCB when created from zone 3.2.5 - smoke callbacks - useRanks option + 3.2.6 - inBuiltup analogon to cloner + INTEGRATES AUTOMATICALLY WITH playerScore @@ -1277,7 +1279,7 @@ function csarManager.createCSARMissionFromZone(theZone) if theZone.onRoad then mPoint.x, mPoint.z = land.getClosestPointOnRoads('roads',mPoint.x, mPoint.z) elseif theZone.inPopulated then - local aPoint = theZone:createRandomPointInPopulatedZone(theZone.clearance, theZone.maxTries) + local aPoint = theZone:createRandomPointInPopulatedZone(theZone.clearance) -- no more maxTries: theZone.maxTries) mPoint = aPoint -- safety in case we need to mod aPoint end local theMission = csarManager.createCSARMissionData( @@ -1409,12 +1411,28 @@ function csarManager.readCSARZone(theZone) theZone.triggerMethod = theZone:getStringFromZoneProperty("triggerMethod", "change") theZone.rndLoc = theZone:getBoolFromZoneProperty("rndLoc", true) theZone.onRoad = theZone:getBoolFromZoneProperty("onRoad", false) - theZone.inPopulated = theZone:getBoolFromZoneProperty("inPopulated", false) - theZone.clearance = theZone:getNumberFromZoneProperty("clearance", 10) - theZone.maxTries = theZone:getNumberFromZoneProperty("maxTries", 20) + if theZone:hasProperty("onRoads") then + theZone.onRoad = theZone:getBoolFromZoneProperty("onRoads", false) + end + + -- emulate inBuiltup from clone Zone + if theZone:hasProperty("inPopulated") or theZone:hasProperty("clearance") or theZone:hasProperty("inBuiltup")then + if theZone:hasProperty("inPopulated") then + theZone.inPopulated = theZone:getBoolFromZoneProperty("inPopulated", false) + else + theZone.inPopulated = true -- presence of clearance forces it to true + end + theZone.clearance = theZone:getNumberFromZoneProperty("clearance", 10) + if theZone:hasProperty("inBuiltUp") then + theZone.clearance = theZone:getNumberFromZoneProperty("inBuiltup", 10) + end + end + -- maxTries is decommed +-- theZone.maxTries = theZone:getNumberFromZoneProperty("maxTries", 20) if theZone.onRoad and theZone.inPopulated then trigger.action.outText("warning: competing 'onRoad' and 'inPopulated' attributes in zone <" .. theZone.name .. ">. Using 'onRoad'.", 30) + theZone.inPopulated = false end -- add to list of startable csar @@ -1423,27 +1441,6 @@ function csarManager.readCSARZone(theZone) end if (not deferred) then - --[[-- - local mPoint = theZone:getPoint() - if theZone.rndLoc then mPoint = theZone:createRandomPointInZone() end - if theZone.onRoad then - mPoint.x, mPoint.z = land.getClosestPointOnRoads('roads',mPoint.x, mPoint.z) - elseif theZone.inPopulated then - local aPoint = theZone:createRandomPointInPopulatedZone(theZone.clearance, theZone.maxTries) - mPoint = aPoint -- safety in case we need to mod aPoint - end - local theMission = csarManager.createCSARMissionData( - mPoint, - theZone.csarSide, - theZone.csarFreq, - theZone.csarName, - theZone.numCrew, - theZone.timeLimit, - theZone.csarMapMarker, - 0.1, -- theZone.radius, - nil) -- parashoo unit - csarManager.addMission(theMission, theZone) ---]]-- local theMission = csarManager.createCSARMissionFromZone(theZone) csarManager.addMission(theMission, theZone) end diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index ce3077a..642828c 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "3.0.3" +dcsCommon.version = "3.0.5" --[[-- VERSION HISTORY 3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false - point2text new intsOnly option @@ -13,6 +13,10 @@ dcsCommon.version = "3.0.3" 3.0.3 - createStaticObjectForCoalitionInRandomRing() returns x and z - isTroopCarrier() also supports 'helo' keyword - new createTakeOffFromGroundRoutePointData() +3.0.4 - getGroupLocation() hardened, optional verbose +3.0.5 - new getNthItem() + - new getFirstItem() + --]]-- -- dcsCommon is a library of common lua functions @@ -184,6 +188,18 @@ dcsCommon.version = "3.0.3" return choices[rtnVal] -- return indexed end + function dcsCommon.getNthItem(theSet, n) + local count = 1 + for key, value in pairs(theSet) do + if count == n then return value end + count = count + 1 + end + return nil + end + + function dcsCommon.getFirstItem(theSet) + return dcsCommon.getNthItem(theSet, 1) + end function dcsCommon.getSizeOfTable(theTable) local count = 0 @@ -888,7 +904,8 @@ dcsCommon.version = "3.0.3" -- get group location: get the group's location by -- accessing the fist existing, alive member of the group that it finds - function dcsCommon.getGroupLocation(group) + function dcsCommon.getGroupLocation(group, verbose, gName) + if not verbose then verbose = false end -- nifty trick from mist: make this work with group and group name if type(group) == 'string' then -- group name group = Group.getByName(group) @@ -896,12 +913,18 @@ dcsCommon.version = "3.0.3" -- get all units local allUnits = group:getUnits() + if not allUnits then + if verbose then + trigger.action.outText("++++common: no group location for <" .. gName .. ">, skipping.", 30) + end + return nil + end -- iterate through all members of group until one is alive and exists for index, theUnit in pairs(allUnits) do if (theUnit:isExist() and theUnit:getLife() > 0) then return theUnit:getPosition().p - end; + end end -- if we get here, there was no live unit diff --git a/modules/groundExplosion.lua b/modules/groundExplosion.lua index 8e7ad51..f0c1e38 100644 --- a/modules/groundExplosion.lua +++ b/modules/groundExplosion.lua @@ -1,5 +1,5 @@ groundExplosion = {} -groundExplosion.version = "1.0.0" +groundExplosion.version = "1.1.0" groundExplosion.requiredLibs = { "dcsCommon", "cfxZones", @@ -9,6 +9,8 @@ groundExplosion.zones = {} --[[-- Version History 1.0.0 - Initial version + 1.0.1 - fixed lib check for objectDestructDetector + 1.1.0 - new flares attribute --]]-- @@ -28,6 +30,9 @@ function groundExplosion.addExplosion(theZone) end theZone.duration = theZone:getNumberFromZoneProperty("duration", 0) theZone.aglMin, theZone.aglMax = theZone:getPositiveRangeFromZoneProperty("AGL", 1,1) + if theZone:hasProperty("flares") then + theZone.flareMin, theZone.flareMax = theZone:getPositiveRangeFromZoneProperty("flares", 0-3) + end end -- @@ -38,6 +43,16 @@ function groundExplosion.doBoom(args) local power = args[2] local theZone = args[3] trigger.action.explosion(loc, power) + if theZone.flareMin then + local flareNum = cfxZones.randomInRange(theZone.flareMin, theZone.flareMax) + if flareNum > 0 then + for i=1, flareNum do + local azimuth = math.random(360) + azimuth = azimuth * 0.0174533 -- in rads + trigger.action.signalFlare(loc, 2, azimuth) -- 2 = white + end + end + end end function groundExplosion.startBoom(theZone) @@ -82,7 +97,7 @@ end function groundExplosion.start() if not dcsCommon.libCheck("cfx groundExplosion", - cfxObjectDestructDetector.requiredLibs) then + groundExplosion.requiredLibs) then return false end diff --git a/modules/groundTroops.lua b/modules/groundTroops.lua index 4a501e7..f97a3ec 100644 --- a/modules/groundTroops.lua +++ b/modules/groundTroops.lua @@ -1,5 +1,5 @@ cfxGroundTroops = {} -cfxGroundTroops.version = "1.7.8" +cfxGroundTroops.version = "2.0.0" cfxGroundTroops.ups = 1 cfxGroundTroops.verbose = false cfxGroundTroops.requiredLibs = { @@ -22,52 +22,15 @@ cfxGroundTroops.requiredLibs = { -- module cfxGroundTroops.deployedTroops = {} -- indexed by group name +cfxGroundTroops.jtacCB = {} -- jtac callbacks, to be implemented --[[-- version history - 1.3.0 - added "wait-" prefix to have toops do nothing - - added lazing - 1.3.1 - sound for lazing msg is "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" - - lazing --> lasing in text - 1.3.2 - set ups to 2 - 1.4.0 - queued updates except for lazers - 1.4.1 - makeTroopsEngageZone now issues hold before moving on 5 seconds later - - getTroopReport - - include size of group - 1.4.2 - uses unitIsInfantry from dcsCommon - 1.5.0 - new scheduled updates per troop to reduce processor load - - tiebreak code - 1.5.1 - small bugfix in scheduled code - 1.5.2 - checkSchedule - - speed warning in scheduler - - go off road when speed warning too much - 1.5.3 - monitor troops - - managed queue for ground troops - - on second switch to offroad now removed from MQ - 1.5.4 - removed debugging messages - 1.5.5 - removed bug in troop report reading nil destination - 1.6.0 - check modules - 1.6.1 - troopsCallback management so you can be informed if a - troop you have added to the pool is dead or has achieved a goal. - callback will list reasons "dead" and "arrived" - updateAttackers - 1.6.2 - also accept 'lase' as 'laze', translate directly - 1.7.0 - now can use groundTroopsConfig zone - 1.7.1 - addTroopsDeadCallback() renamed to addTroopsCallback() - - invokeCallbacksFor also accepts and passes on data block - - troops is always passed in data block as .troops - 1.7.2 - callback when group is neutralized on guard orders - - callback when group is being engaged under guard orders - 1.7.3 - callbacks for lase:tracking and lase:stop - 1.7.4 - verbose flag, warnings suppressed - 1.7.5 - some troop.group hardening with isExist() - 1.7.6 - fixed switchToOffroad - 1.7.7 - no longer case sensitive for orders - 1.7.7 - updateAttackers() now inspects 'moving' status and invokes makeTroopsEngageZone - - makeTroopsEngageZone() sets 'moving' status to true - - createGroundTroops() sets moving status to false - - updateZoneAttackers() uses moving - 1.7.8 - better guards before invoking ownedZones + + 2.0.0 - dmlZones + - jtacSound + - clanup + - jtacVerbose an entry into the deployed troop table has the following attributes - group - the group @@ -113,63 +76,47 @@ cfxGroundTroops.deployedTroops = {} -- indexed by group name -- queued will work one every pass (except for lazed), distributing the load much better -- schedueld installs a callback for each group separately and thus distributes the load over time much better -cfxGroundTroops.queuedUpdates = false -- set to true to process one group per turn. To work this way, scheduledUpdates must be false -cfxGroundTroops.scheduledUpdates = true -- set to false to allow queing of standard updates. overrides queuedUpdates -cfxGroundTroops.monitorNumbers = false -- set to true to debug managed group size +function cfxGroundTroops.invokeCallbacks(ID, jtac, tgt, data) + -- IS is aqui, lost, dead, jtac died. jtac is group, tgt is unit, data is rest + for idx, cb in pairs(cfxGroundTroops.jtacCB) do + cb(ID, jtac, tgt, data) + end +end -cfxGroundTroops.standardScheduleInterval = 30 -- 30 seconds between calls -cfxGroundTroops.guardUpdateInterval = 30 -- every 30 seconds we check up on guards -cfxGroundTroops.trackingUpdateInterval = 0.5 -- 0.5 seconds for lazer tracking etc +function cfxGroundTroops.addJtacCB(theCB) + table.insert(cfxGroundTroops.jtacCB, theCB) +end -cfxGroundTroops.maxManagedTroops = 67 -- -1 is infinite, any positive number turn on cap on managed troops and palces excess troops in queue cfxGroundTroops.troopQueue = {} -- FIFO stack -- return the best tracking interval for this type of orders --- --- READ CONFIG ZONE TO OVERRIDE SETTING --- function cfxGroundTroops.readConfigZone() - -- note: must match exactly!!!! local theZone = cfxZones.getZoneByName("groundTroopsConfig") if not theZone then - if cfxGroundTroops.verbose then - trigger.action.outText("***gndT: NO config zone!", 30) - end theZone = cfxZones.createSimpleZone("groundTroopsConfig") end - -- ok, for each property, load it if it exists - if cfxZones.hasProperty(theZone, "queuedUpdates") then - cfxGroundTroops.queuedUpdates = cfxZones.getBoolFromZoneProperty(theZone, "queuedUpdates", false) - end - - if cfxZones.hasProperty(theZone, "scheduledUpdates") then - cfxGroundTroops.scheduledUpdates = cfxZones.getBoolFromZoneProperty(theZone, "scheduledUpdates", false) - end - - if cfxZones.hasProperty(theZone, "maxManagedTroops") then - cfxGroundTroops.maxManagedTroops = cfxZones.getNumberFromZoneProperty(theZone, "maxManagedTroops", 65) - end - - if cfxZones.hasProperty(theZone, "monitorNumbers") then - cfxGroundTroops.monitorNumbers = cfxZones.getBoolFromZoneProperty(theZone, "monitorNumbers", false) - end - - if cfxZones.hasProperty(theZone, "standardScheduleInterval") then - cfxGroundTroops.standardScheduleInterval = cfxZones.getNumberFromZoneProperty(theZone, "standardScheduleInterval", 30) - end - - if cfxZones.hasProperty(theZone, "guardUpdateInterval") then - cfxGroundTroops.guardUpdateInterval = cfxZones.getNumberFromZoneProperty(theZone, "guardUpdateInterval", 30) - end - - if cfxZones.hasProperty(theZone, "trackingUpdateInterval") then - cfxGroundTroops.trackingUpdateInterval = cfxZones.getNumberFromZoneProperty(theZone, "trackingUpdateInterval", 0.5) - end - - if cfxZones.hasProperty(theZone, "verbose") then - cfxGroundTroops.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) - end + cfxGroundTroops.queuedUpdates = theZone:getBoolFromZoneProperty("queuedUpdates", false) + cfxGroundTroops.scheduledUpdates = theZone:getBoolFromZoneProperty("scheduledUpdates", false) + cfxGroundTroops.maxManagedTroops = theZone:getNumberFromZoneProperty("maxManagedTroops", 67) + cfxGroundTroops.monitorNumbers = theZone:getBoolFromZoneProperty("monitorNumbers", false) + cfxGroundTroops.standardScheduleInterval = theZone:getNumberFromZoneProperty("standardScheduleInterval", 30) + cfxGroundTroops.guardUpdateInterval = theZone:getNumberFromZoneProperty("guardUpdateInterval", 30) + cfxGroundTroops.trackingUpdateInterval = theZone:getNumberFromZoneProperty("trackingUpdateInterval", 0.5) + + cfxGroundTroops.jtacSound = theZone:getStringFromZoneProperty("jtacSound", "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") + cfxGroundTroops.jtacVerbose = theZone:getBoolFromZoneProperty("jtacVerbose", true) + cfxGroundTroops.laseCode = theZone:getNumberFromZoneProperty("jtacLaserCode", 1688) + if theZone:hasProperty("lazeCode") then + cfxGroundTroops.laseCode = theZone:getNumberFromZoneProperty("lazeCode", 1688) + end + if theZone:hasProperty("laseCode") then + cfxGroundTroops.laseCode = theZone:getNumberFromZoneProperty("laseCode", 1688) + end + if theZone:hasProperty("laserCode") then + cfxGroundTroops.laseCode = theZone:getNumberFromZoneProperty("laserCode", 1688) + end + cfxGroundTroops.verbose = theZone.verbose if cfxGroundTroops.verbose then trigger.action.outText("+++gndT: read config zone!", 30) @@ -355,8 +302,7 @@ function cfxGroundTroops.updateAttackers(troop) troop.orders = "guard" return end - - + -- if we get here, we need no change end @@ -437,7 +383,7 @@ function cfxGroundTroops.findLazeTarget(troop) -- iterate through the list until we find the first target -- that fits the bill and return it --- trigger.action.outText("+++ looking at " .. #enemyGroups .. " laze groups", 30) + for i=1, #enemyGroups do -- get all units for this group local aGroup = enemyGroups[i].group -- remember, they are in a {dist, group} tuple @@ -448,12 +394,9 @@ function cfxGroundTroops.findLazeTarget(troop) -- unit lives -- now, we need to filter infantry. we do this by -- pre-fetching the typeString - --troop.lazeTargetType = aUnit:getTypeName() -- and checking if the name contains some infantry- -- typical strings. Idea taken from JTAC script local isInfantry = dcsCommon.unitIsInfantry(theUnit) - - if not isInfantry then -- this is a vehicle, is it in line of sight? -- raise the point 2m above ground for both points @@ -466,16 +409,11 @@ function cfxGroundTroops.findLazeTarget(troop) -- the nearest group to us in range -- that is visible! return aUnit - else - --trigger.action.outText("+++ ".. aUnit:getName() .."cant be seen", 30) end -- if visible - else - -- trigger.action.outText("+++ ".. aUnit:getName() .." (".. troop.lazeTargetType .. ") is infantry", 30) - end -- if not infantry + end -- if infantry end -- if alive end -- for all units end -- for all enemy groups - --trigger.action.outText("+++ find nearest laze target did not find anything to laze", 30) return nil -- no unit found end @@ -499,10 +437,12 @@ function cfxGroundTroops.trackLazer(troop) if not troop.lazerPointer then local there = troop.lazeTarget:getPoint() - troop.lazerPointer = Spot.createLaser(troop.lazingUnit,{x = 0, y = 2, z = 0}, there, 1688) + troop.lazerPointer = Spot.createLaser(troop.lazingUnit,{x = 0, y = 2, z = 0}, there, cfxGroundTroops.laseCode) troop.lazeTargetType = troop.lazeTarget:getTypeName() - trigger.action.outTextForCoalition(troop.side, troop.name .. " tally target - lasing " .. troop.lazeTargetType .. "!", 30) - trigger.action.outSoundForCoalition(troop.side, "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") + if cfxGroundTroops.jtacVerbose then + trigger.action.outTextForCoalition(troop.side, troop.name .. " tally target - lasing " .. troop.lazeTargetType .. ", code " .. cfxGroundTroops.laseCode .. "!", 30) + trigger.action.outSoundForCoalition(troop.side, cfxGroundTroops.jtacSound) -- "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") + end troop.lastLazerSpot = there -- remember last spot local data = {} data.enemy = troop.lazeTarget @@ -510,9 +450,7 @@ function cfxGroundTroops.trackLazer(troop) cfxGroundTroops.invokeCallbacksFor("lase:tracking", troop, data) return end - - -- if true then return end - + -- if we get here, we update the lazerPointer local there = troop.lazeTarget:getPoint() -- we may only want to update the laser spot when dist > trigger @@ -531,17 +469,17 @@ function cfxGroundTroops.updateLaze(troop) else cfxGroundTroops.lazerOff(troop) troop.lazeTarget = nil - trigger.action.outTextForCoalition(troop.side, troop.name .. " reports lasing " .. troop.lazeTargetType .. " interrupted. Re-acquiring.", 30) - trigger.action.outSoundForCoalition(troop.side, "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") + if cfxGroundTroops.jtacVerbose then + trigger.action.outTextForCoalition(troop.side, troop.name .. " reports lasing " .. troop.lazeTargetType .. " interrupted. Re-acquiring.", 30) + trigger.action.outSoundForCoalition(troop.side, cfxGroundTroops.jtacSound) -- "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") + end troop.lazingUnit = nil cfxGroundTroops.invokeCallbacksFor("lase:stop", troop) return -- we'll re-acquire through a new unit next round end end - -- if we get here, a lazing unit - --local here = troop.lazingUnit:getPoint() - + -- if we get here, a lazing unit if troop.lazeTarget then -- check if that target is alive and in range if troop.lazeTarget:isExist() and troop.lazeTarget:getLife() >= 1 then @@ -551,8 +489,10 @@ function cfxGroundTroops.updateLaze(troop) local there = troop.lazeTarget:getPoint() if dcsCommon.dist(here, there) > troop.range then -- troop out of range - trigger.action.outTextForCoalition(troop.side, troop.name .. " lost sight of lazed target " .. troop.lazeTargetType, 30) - trigger.action.outSoundForCoalition(troop.side, "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") + if cfxGroundTroops.jtacVerbose then + trigger.action.outTextForCoalition(troop.side, troop.name .. " lost sight of lazed target " .. troop.lazeTargetType, 30) + trigger.action.outSoundForCoalition(troop.side, cfxGroundTroops.jtacSound) -- "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") + end troop.lazeTarget = nil cfxGroundTroops.lazerOff(troop) troop.lazingUnit = nil @@ -565,8 +505,10 @@ function cfxGroundTroops.updateLaze(troop) return else -- target died - trigger.action.outTextForCoalition(troop.side, troop.name .. " confirms kill for " .. troop.lazeTargetType, 30) - trigger.action.outSoundForCoalition(troop.side, "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") + if cfxGroundTroops.jtacVerbose then + trigger.action.outTextForCoalition(troop.side, troop.name .. " confirms kill for " .. troop.lazeTargetType, 30) + trigger.action.outSoundForCoalition(troop.side, cfxGroundTroops.jtacSound) -- "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") + end troop.lazeTarget = nil cfxGroundTroops.lazerOff(troop) troop.lazingUnit = nil @@ -584,8 +526,7 @@ end function cfxGroundTroops.updateWait(troop) - -- currently nothing to do - + -- currently nothing to do end function cfxGroundTroops.updateTroops(troop) @@ -616,12 +557,6 @@ function cfxGroundTroops.updateTroops(troop) end --- --- we have to systems to process during update: --- once all, and one per turn, with the exception --- of lazers, who get updated every turn --- - -- -- all at once -- @@ -754,14 +689,12 @@ function cfxGroundTroops.updateSingleScheduled(params) -- now, check if still alive if not dcsCommon.isGroupAlive(group) then -- group dead, no longer updates - --trigger.action.outText("+++groundT NOTE: <".. troops.group:getName() .."> dead, removing", 30) cfxGroundTroops.invokeCallbacksFor("dead", troops) -- notify anyone who is interested that we are no longer proccing these cfxGroundTroops.removeTroopsFromPool(troops) return -- nothing else to do end -- now, execute the update itself, standard update - --trigger.action.outText("+++groundT: singleU troop <".. troops.group:getName() .."> with orders <" .. troops.orders .. ">", 30) cfxGroundTroops.updateTroops(troops) -- check max speed of group. if < 0.1 then note and increase @@ -775,7 +708,6 @@ function cfxGroundTroops.updateSingleScheduled(params) if troops.speedWarning > 5 then -- make me 5 lastOrder = timer.getTime() - troops.lastOrderDate - --trigger.action.outText("+++groundT WARNING: <".. troops.group:getName() .."> (S:".. troops.side .. ") to " .. troops.destination.name .. ": stopped for " .. troops.speedWarning .. " iters, orderage=" .. lastOrder, 30) -- this may be a matter of too many waypoints. -- maybe issue orders to go to their destination directly? -- now force an order to go directly. @@ -784,17 +716,15 @@ function cfxGroundTroops.updateSingleScheduled(params) -- we already switched to off-road. take me -- out of the managed queue, I'm not going -- anywhere - -- trigger.action.outText("+++groundT <".. troops.group:getName() .."> is going nowhere. Removed from managed troops", 30) cfxGroundTroops.removeTroopsFromPool(troops) else cfxGroundTroops.switchToOffroad(troops) - -- trigger.action.outText("+++groundT <".. troops.group:getName() .."> SWITCHED TO OFFROAD", 30) troops.isOffroad = true -- so we know that we already did that end end end - -- now reschedule updte for my best time + -- now reschedule update for my best time local updateTime = cfxGroundTroops.getScheduleInterval(troops.orders) troops.updateID = timer.scheduleFunction(cfxGroundTroops.updateSingleScheduled, params, timer.getTime() + updateTime) end @@ -818,11 +748,9 @@ end function cfxGroundTroops.checkPileUp() -- schedule my next call - --trigger.action.outText("+++groundT: pileup check", 30) timer.scheduleFunction(cfxGroundTroops.checkPileUp, {}, timer.getTime() + 60) local thePiles = {} if not cfxOwnedZones then - -- trigger.action.outText("+++groundT: pileUp - owned zones not yet ready", 30) return end @@ -945,11 +873,6 @@ function cfxGroundTroops.getTroopReport(theSide, ignoreInfantry) return report end - --- --- CREATE / ADD / REMOVE --- - -- -- createGroundTroop -- use this to create a cfxGroundTroops from a dcs group @@ -1100,13 +1023,12 @@ function cfxGroundTroops.manageQueues() -- if we here, there are items waiting in the queue while dcsCommon.getSizeOfTable(cfxGroundTroops.deployedTroops) < cfxGroundTroops.maxManagedTroops and #cfxGroundTroops.troopQueue > 0 do - -- trnasfer items from the front to the managed queue + -- transfer items from the front to the managed queue local theTroops = cfxGroundTroops.troopQueue[1] table.remove(cfxGroundTroops.troopQueue, 1) if theTroops.group:isExist() then cfxGroundTroops.deployedTroops[theTroops.group:getName()] = theTroops end - -- trigger.action.outText("+++gT: dequed and activaed " .. theTroops.group:getName(), 30) end end diff --git a/modules/groupTrackers.lua b/modules/groupTrackers.lua index 58b32c6..a2b0733 100644 --- a/modules/groupTrackers.lua +++ b/modules/groupTrackers.lua @@ -126,6 +126,7 @@ end function groupTracker.addGroupToTrackerNamed(theGroup, trackerName) if not trackerName then trigger.action.outText("+++gTrk: nil tracker in addGroupToTrackerNamed", 30) + return end if not theGroup then trigger.action.outText("+++gTrk: no group in addGroupToTrackerNamed <" .. trackerName .. ">", 30) @@ -543,6 +544,13 @@ function groupTracker.trackGroupsInZone(theZone) local trackerName = cfxZones.getStringFromZoneProperty(theZone, "addToTracker:", "") local theGroups = cfxZones.allGroupsInZone(theZone, nil) +--[[-- trigger.action.outText("Groups in zone <" .. theZone.name .. ">:", 30) + local msg = " :: " + for idx, aGroup in pairs (theGroups) do + msg = msg .. " <" .. aGroup:getName() .. ">" + end + trigger.action.outText(msg, 30) +--]]-- -- now init array processing local trackerNames = {} @@ -613,6 +621,18 @@ function groupTracker.start() groupTracker.trackGroupsInZone(aZone) -- process attributes end + -- verbose debugging: + -- show who's tracking who + for idx, theZone in pairs(groupTracker.trackers) do + if groupTracker.verbose or theZone.verbose then + local msg = " - Tracker <" .. theZone.name .. ">: " + for idx, theGroup in pairs(theZone.trackedGroups) do + msg = msg .. "<" .. theGroup:getName() .. "> " + end + trigger.action.outText(msg, 30) + end + end + -- update all cloners and spawned clones from file if persistence then -- sign up for persistence diff --git a/modules/jtacGrpUI.lua b/modules/jtacGrpUI.lua index 49f0258..84bb1f1 100644 --- a/modules/jtacGrpUI.lua +++ b/modules/jtacGrpUI.lua @@ -1,15 +1,24 @@ jtacGrpUI = {} -jtacGrpUI.version = "1.0.2" +jtacGrpUI.version = "2.0.0" +jtacGrpUI.requiredLibs = { + "dcsCommon", -- always + "cfxZones", + "cfxGroundTroops", +} --[[-- VERSION HISTORY - 1.0.2 - also include idling JTACS - add positional info when using owned zones - + - 2.0.0 - dmlZones + - sanity checks upon load + - eliminated cfxPlayer dependence + - clean-up + - jtacSound --]]-- -- find & command cfxGroundTroops-based jtacs -- UI installed via OTHER for all groups with players -- module based on xxxGrpUI -jtacGrpUI.groupConfig = {} -- all inited group private config data +jtacGrpUI.groupConfig = {} -- all inited group private config data, indexed by group name. jtacGrpUI.simpleCommands = true -- if true, f10 other invokes directly -- @@ -24,6 +33,9 @@ function jtacGrpUI.resetConfig(conf) end function jtacGrpUI.createDefaultConfig(theGroup) + if not theGroup then return nil end + if not Group.isExist(theGroup) then return end + local conf = {} conf.theGroup = theGroup conf.name = theGroup:getName() @@ -32,8 +44,8 @@ function jtacGrpUI.createDefaultConfig(theGroup) jtacGrpUI.resetConfig(conf) - conf.mainMenu = nil; -- this is where we store the main menu if we branch - conf.myCommands = nil; -- this is where we store the commands if we branch + conf.mainMenu = nil; -- root + conf.myCommands = nil; -- commands branch return conf end @@ -41,15 +53,15 @@ end -- getConfigFor group will allocate if doesn't exist in DB -- and add to it function jtacGrpUI.getConfigForGroup(theGroup) - if not theGroup then + if not theGroup or (not Group.isExist(theGroup))then trigger.action.outText("+++WARNING: jtacGrpUI nil group in getConfigForGroup!", 30) return nil end local theName = theGroup:getName() - local c = jtacGrpUI.getConfigByGroupName(theName) -- we use central accessor + local c = jtacGrpUI.getConfigByGroupName(theName) if not c then c = jtacGrpUI.createDefaultConfig(theGroup) - jtacGrpUI.groupConfig[theName] = c -- should use central accessor... + jtacGrpUI.groupConfig[theName] = c end return c end @@ -66,16 +78,13 @@ function jtacGrpUI.getConfigForUnit(theUnit) trigger.action.outText("+++WARNING: jtacGrpUI nil unit in getConfigForUnit!", 30) return nil end - local theGroup = theUnit:getGroup() return getConfigForGroup(theGroup) end --- -- -- M E N U H A N D L I N G -- ========================= --- -- function jtacGrpUI.clearCommsSubmenus(conf) if conf.myCommands then @@ -95,8 +104,7 @@ function jtacGrpUI.removeCommsFromConfig(conf) end end --- this only works in single-unit groups. may want to check if group --- has disappeared +-- this only works in single-unit player groups. function jtacGrpUI.removeCommsForUnit(theUnit) if not theUnit then return end if not theUnit:isExist() then return end @@ -112,11 +120,21 @@ function jtacGrpUI.removeCommsForGroup(theGroup) jtacGrpUI.removeCommsFromConfig(conf) end --- --- set main root in F10 Other. All sub menus click into this --- function jtacGrpUI.isEligibleForMenu(theGroup) - return true + if jtacGrpUI.jtacTypes == "all" or + jtacGrpUI.jtacTypes == "any" then return true end + if dcsCommon.stringStartsWith(jtacGrpUI.jtacTypes, "hel", true) then + local cat = theGroup:getCategory() + return cat == 1 + end + if dcsCommon.stringStartsWith(jtacGrpUI.jtacTypes, "plan", true) then + local cat = theGroup:getCategory() + return cat == 0 + end + if jtacGrpUI.verbose then + trigger.action.outText("+++jGUI: unknown jtacTypes <" .. jtacGrpUI.jtacTypes .. "> -- allowing access to group <" .. theGroup:getName() ..">", 30) + end + return true -- for later expansion end function jtacGrpUI.setCommsMenuForUnit(theUnit) @@ -131,38 +149,25 @@ function jtacGrpUI.setCommsMenuForUnit(theUnit) end function jtacGrpUI.setCommsMenu(theGroup) - -- depending on own load state, we set the command structure - -- it begins at 10-other, and has 'jtac' as main menu with submenus - -- as required if not theGroup then return end - if not theGroup:isExist() then return end - - -- we test here if this group qualifies for - -- the menu. if not, exit + if not Group.isExist(theGroup) then return end if not jtacGrpUI.isEligibleForMenu(theGroup) then return end local conf = jtacGrpUI.getConfigForGroup(theGroup) - conf.id = theGroup:getID(); -- we do this ALWAYS so it is current even after a crash --- trigger.action.outText("+++ setting group <".. conf.theGroup:getName() .. "> jtac command", 30) + conf.id = theGroup:getID(); -- we always do this ALWAYS if jtacGrpUI.simpleCommands then -- we install directly in F-10 other 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, nil, jtacGrpUI.redirectCommandX, {conf, "lasing report"}) conf.myMainMenu = theCommand end return end - - + -- 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') @@ -178,12 +183,6 @@ function jtacGrpUI.setCommsMenu(theGroup) end function jtacGrpUI.addSubMenus(conf) - -- add menu items to choose from after - -- user clickedf on MAIN MENU. In this implementation - -- they all result invoked methods - - - local commandTxt = "jtac Lasing Report" local theCommand = missionCommands.addCommandForGroup( conf.id, @@ -193,24 +192,8 @@ function jtacGrpUI.addSubMenus(conf) {conf, "lasing report"} ) table.insert(conf.myCommands, theCommand) ---[[-- - commandTxt = "This is another important command" - theCommand = missionCommands.addCommandForGroup( - conf.id, - commandTxt, - conf.myMainMenu, - jtacGrpUI.redirectCommandX, - {conf, "Sub2"} - ) - table.insert(conf.myCommands, theCommand) ---]]-- end --- --- each menu item has a redirect and timed invoke to divorce from the --- no-debug zone in the menu invocation. Delay is .1 seconds --- - function jtacGrpUI.redirectCommandX(args) timer.scheduleFunction(jtacGrpUI.doCommandX, args, timer.getTime() + 0.1) end @@ -219,25 +202,25 @@ function jtacGrpUI.doCommandX(args) local conf = args[1] -- < conf in here local what = args[2] -- < second argument in here local theGroup = conf.theGroup --- trigger.action.outTextForGroup(conf.id, "+++ groupUI: processing comms menu for <" .. what .. ">", 30) local targetList = jtacGrpUI.collectJTACtargets(conf, true) -- iterate the list if #targetList < 1 then trigger.action.outTextForGroup(conf.id, "No targets are currently being lased", 30) + trigger.action.outSoundForGroup(conf.id, jtacGrpUI.jtacSound) return end local desc = "JTAC Target Report:\n" --- trigger.action.outTextForGroup(conf.id, "Target Report:", 30) for i=1, #targetList do local aTarget = targetList[i] if aTarget.idle then desc = desc .. "\n" .. aTarget.jtacName .. aTarget.posInfo ..": no target" else - desc = desc .. "\n" .. aTarget.jtacName .. aTarget.posInfo .." lasing " .. aTarget.lazeTargetType .. " [" .. aTarget.range .. "nm at " .. aTarget.bearing .. "°]" + desc = desc .. "\n" .. aTarget.jtacName .. aTarget.posInfo .." lasing " .. aTarget.lazeTargetType .. " [" .. aTarget.range .. "nm at " .. aTarget.bearing .. "°]," .. " code=" .. cfxGroundTroops.laseCode end end trigger.action.outTextForGroup(conf.id, desc .. "\n", 30) + trigger.action.outSoundForGroup(conf.id, jtacGrpUI.jtacSound) end function jtacGrpUI.collectJTACtargets(conf, includeIdle) @@ -249,14 +232,14 @@ function jtacGrpUI.collectJTACtargets(conf, includeIdle) local theJTACS = {} for idx, troop in pairs(cfxGroundTroops.deployedTroops) do if troop.coalition == conf.coalition - and troop.orders == "laze" - and troop.lazeTarget - and troop.lazeTarget:isExist() + and troop.orders == "laze" + and troop.lazeTarget + and troop.lazeTarget:isExist() then table.insert(theJTACS, troop) elseif troop.coalition == conf.coalition - and troop.orders == "laze" - and includeIdle + and troop.orders == "laze" + and includeIdle then -- we also include idlers table.insert(theJTACS, troop) @@ -277,7 +260,7 @@ function jtacGrpUI.collectJTACtargets(conf, includeIdle) local jtacLoc = dcsCommon.getGroupLocation(troop.group) local nearestZone = cfxOwnedZones.getNearestOwnedZoneToPoint(jtacLoc) if nearestZone then - local ozRange = dcsCommon.dist(jtacLoc, nearestZone.point) * 0.000621371 + local ozRange = dcsCommon.dist(jtacLoc, nearestZone.point) * 0.000621371 -- meters to nm ozRange = math.floor(ozRange * 10) / 10 local relPos = dcsCommon.compassPositionOfARelativeToB(jtacLoc, nearestZone.point) aTarget.posInfo = " (" .. ozRange .. "nm " .. relPos .. " of " .. nearestZone.name .. ")" @@ -295,7 +278,6 @@ function jtacGrpUI.collectJTACtargets(conf, includeIdle) aTarget.range = aTarget.range * 0.000621371 -- meter to miles aTarget.range = math.floor(aTarget.range * 10) / 10 aTarget.bearing = dcsCommon.bearingInDegreesFromAtoB(here, there) - --aTarget.jtacName = troop.name aTarget.lazeTargetType = troop.lazeTargetType end table.insert(targetList, aTarget) @@ -309,87 +291,82 @@ function jtacGrpUI.collectJTACtargets(conf, includeIdle) end -- --- G R O U P M A N A G E M E N T +-- event handler - simplified, only for player birth -- --- Group Management is required to make sure all groups --- receive a comms menu and that they receive a clean-up --- when required --- --- Callbacks are provided by cfxPlayer module to which we --- subscribe during init --- -function jtacGrpUI.playerChangeEvent(evType, description, player, data) - --trigger.action.outText("+++ groupUI: received <".. evType .. "> Event", 30) - if evType == "newGroup" then - -- initialized attributes are in data as follows - -- .group - new group - -- .name - new group's name - -- .primeUnit - the unit that trigggered new group appearing - -- .primeUnitName - name of prime unit - -- .id group ID - --theUnit = data.primeUnit - jtacGrpUI.setCommsMenu(data.group) --- trigger.action.outText("+++ groupUI: added " .. theUnit:getName() .. " to comms menu", 30) - return - end - - if evType == "removeGroup" then - -- data is the player record that no longer exists. it consists of - -- .name - -- we must remove the comms menu for this group else we try to add another one to this group later - local conf = jtacGrpUI.getConfigByGroupName(data.name) - - if conf then - jtacGrpUI.removeCommsFromConfig(conf) -- remove menus - jtacGrpUI.resetConfig(conf) -- re-init this group for when it re-appears - else - trigger.action.outText("+++ jtacUI: can't retrieve group <" .. data.name .. "> config: not found!", 30) - end - - return - end - - if evType == "leave" then - -- player unit left. we don't care since we only work on group level - -- if they were the only, this is followed up by group disappeared - - end - - if evType == "unit" then - -- player changed units. almost never in MP, but possible in solo - -- because of 1 seconds timing loop - -- will result in a new group appearing and a group disappearing, so we are good - -- may need some logic to clean up old configs and/or menu items - - end +function jtacGrpUI:onEvent(theEvent) + if not theEvent then return end + local theUnit = theEvent.initiator + if not theUnit then return end + local uName = theUnit:getName() + if not theUnit.getPlayerName then return end + if not theUnit:getPlayerName() then return end + -- we now have a player birth event. + local pName = theUnit:getPlayerName() + local theGroup = theUnit:getGroup() + if not theGroup then return end + local gName = theGroup:getName() + if not gName then return end + if jtacGrpUI.verbose then + trigger.action.outText("+++jGUI: birth player. installing JTAC for <" .. pName .. "> on unit <" .. uName .. ">", 30) + end + local conf = jtacGrpUI.getConfigByGroupName(gName) + if conf then + jtacGrpUI.removeCommsFromConfig(conf) -- remove menus + jtacGrpUI.resetConfig(conf) -- re-init this group for when it re-appears + end + jtacGrpUI.setCommsMenu(theGroup) end + -- -- Start -- -function jtacGrpUI.start() +function jtacGrpUI.readConfigZone() + local theZone = cfxZones.getZoneByName("jtacGrpUIConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("jtacGrpUIConfig") + end + + 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") + + jtacGrpUI.verbose = theZone.verbose - -- iterate existing groups so we have a start situation - -- now iterate through all player groups and install the Assault Troop Menu - allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it! - -- contains per group player record. Does not resolve on unit level! - for gname, pgroup in pairs(allPlayerGroups) do - local theUnit = pgroup.primeUnit -- get any unit of that group - jtacGrpUI.setCommsMenuForUnit(theUnit) -- set up - end - -- now install the new group notifier to install Assault Troops menu - - cfxPlayer.addMonitor(jtacGrpUI.playerChangeEvent) - trigger.action.outText("cf/x jtacGrpUI v" .. jtacGrpUI.version .. " started", 30) - end --- +function jtacGrpUI.start() + if not dcsCommon.libCheck then + trigger.action.outText("cfx jtac GUI requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx jtac GUI", jtacGrpUI.requiredLibs) then + return false + end + + jtacGrpUI.readConfigZone() + + local allPlayerUnits = dcsCommon.getAllExistingPlayerUnitsRaw() + for unitName, theUnit in pairs(allPlayerUnits) do + jtacGrpUI.setCommsMenuForUnit(theUnit) + end + + -- now install event handler + world.addEventHandler(jtacGrpUI) + trigger.action.outText("cf/x jtacGrpUI v" .. jtacGrpUI.version .. " started", 30) + return true +end + -- GO GO GO --- -if not cfxGroundTroops then - trigger.action.outText("cf/x jtacGrpUI REQUIRES cfxGroundTroops to work.", 30) -else - jtacGrpUI.start() -end \ No newline at end of file +if not jtacGrpUI.start() then + trigger.action.outText("JTAC GUI failed to start up.", 30) + jtacGrpUI = nil +end + +--[[-- + TODO: + callback into GroundTroops lazing + what is 'simpleCommand' really for? remove or refine +--]]-- \ No newline at end of file diff --git a/modules/milHelo.lua b/modules/milHelo.lua new file mode 100644 index 0000000..988f6d7 --- /dev/null +++ b/modules/milHelo.lua @@ -0,0 +1,333 @@ +milHelo = {} +milHelo.version = "0.0.0" +milHelo.requiredLibs = { + "dcsCommon", + "cfxZones", + "cfxMX", +} +milHelo.zones = {} +milHelo.targets = {} +milHelo.ups = 1 + +function milHelo.addMilHeloZone(theZone) + milHelo.zones[theZone.name] = theZone +end + +function milHelo.addMilTargetZone(theZone) + milHelo.targets[theZone.name] = theZone +end + +function milHelo.partOfGroupDataInZone(theZone, theUnits) -- move to mx? + local zP = cfxZones.getPoint(theZone) + zP = theZone:getDCSOrigin() -- don't use getPoint now. + zP.y = 0 + + for idx, aUnit in pairs(theUnits) do + local uP = {} + uP.x = aUnit.x + uP.y = 0 + uP.z = aUnit.y -- !! y-z + if theZone:pointInZone(uP) then return true end + end + return false +end + +function milHelo.allGroupsInZoneByData(theZone) -- move to MX? + local theGroupsInZone = {} + local count = 0 + for groupName, groupData in pairs(cfxMX.groupDataByName) do + if groupData.units then + if milHelo.partOfGroupDataInZone(theZone, groupData.units) then + theGroupsInZone[groupName] = groupData -- DATA! work on clones! + count = count + 1 + if theZone.verbose then + trigger.action.outText("+++milH: added group <" .. groupName .. "> for zone <" .. theZone.name .. ">", 30) + end + end + end + end + return theGroupsInZone, count +end + +function milHelo.readMilHeloZone(theZone) -- process attributes + -- get mission type. part of milHelo + theZone.msnType = string.lower(theZone:getStringFromZoneProperty("milHelo", "cas")) + -- get all groups inside me + local myGroups, count = milHelo.allGroupsInZoneByData(theZone) + theZone.myGroups = myGroups + theZone.groupCount = count + theZone.coa = theZone:getCoalitionFromZoneProperty("coalition", 0) + theZone.hot = theZone:getBoolFromZoneProperty("hot", true) + theZone.speed = theZone:getNumberFromZoneProperty("speed", 50) -- 110 mph + theZone.alt = theZone:getNumberFromZoneProperty("alt", 100) -- we are always radar alt + -- wipe all existing + for groupName, data in pairs(myGroups) do + local g = Group.getByName(groupName) + if g then + Group.destroy(g) + end + end + if theZone.verbose or milHelo.verbose then + trigger.action.outText("+++milH: processed milHelo zone <" .. theZone.name .. ">", 30) + end +end + +function milHelo.readMilTargetZone(theZone) + + if theZone.verbose or milHelo.verbose then + trigger.action.outText("+++milH: processed TARGET zone <" .. theZone.name .. ">", 30) + end +end + +-- +-- Spawning for a zone +-- +--[[-- +function milHelo.getNthItem(theSet, n) + local count = 1 + for key, value in pairs(theSet) do + if count == n then return value end + count = count + 1 + end + return nil +end +--]]-- + +function milHelo.createCASTask(num, auto) + if not auto then auto = false end + if not num then num = 1 end + local task = {} + task.number = num + task.key = "CAS" + task.id = "EngageTargets" + task.enabled = true + task.auto = auto + local params = {} + params.priority = 0 +-- params.targetTypes = {"Helicopters", "Ground Units", "Light armed ships"} + local targetTypes = {[1] = "Helicopters", [2] = "Ground Units", [3] = "Light armed ships",} + params.targetTypes = targetTypes + + task.params = params + return task +end + +function milHelo.createROETask(num, roe) + if not num then num = 1 end + if not roe then roe = 0 end + local task = {} + task.number = num + task.enabled = true + task.auto = false + task.id = "WrappedAction" + local params = {} + local action = {} + action.id = "Option" + local p2 = {} + p2.value = roe -- 0 = Weapons free + p2.name = 0 -- name 0 = ROE + action.params = p2 + params.action = action + task.params = params + return task +end + +function milHelo.createOrbitTask(num, duration, theZone) + if not num then num = 1 end + local task = {} + task.number = num + task.auto = false + task.id = "ControlledTask" + task.enabled = true + local params = {} + local t2 = {} + t2.id = "Orbit" + local p2 = {} + p2.altitude = theZone.alt + p2.pattern = "Circle" + p2.speed = theZone.speed + p2.altitudeEdited = true + t2.params = p2 + params.task = t2 + params.stopCondition = {} + params.stopCondition.duration = duration + task.params = params + return task +end + +function milHelo.createTakeOffWP(theZone) + local WP = {} + WP.alt = theZone.alt + WP.alt_type = "RADIO" + WP.properties = {} + WP.properties.addopt = {} + WP.action = "From Ground Area" + if theZone.hot then WP.action = "From Ground Area Hot" end + WP.speed = theZone.speed + WP.task = {} + WP.task.id = "ComboTask" + WP.task.params = {} + local tasks = {} +-- local casTask = milHelo.createCASTask(1) +-- tasks[1] = casTask + local roeTask = milHelo.createROETask(1,0) -- 0 = weapons free + tasks[1] = roeTask + WP.task.params.tasks = tasks + -- + WP.type = "TakeOffGround" + if theZone.hot then WP.type = "TakeOffGroundHot" end + p = theZone:getPoint() + WP.x = p.x + WP.y = p.z + WP.ETA = 0 + WP.ETA_locked = false + WP.speed_locked = true + WP.formation_template = "" + return WP +end + + + +function milHelo.createOrbitWP(theZone, targetPoint) + local WP = {} + WP.alt = theZone.alt + WP.alt_type = "RADIO" + WP.properties = {} + WP.properties.addopt = {} + WP.action = "Turning Point" + WP.speed = theZone.speed + WP.task = {} + WP.task.id = "ComboTask" + WP.task.params = {} + -- start params construct + local tasks = {} + local casTask = milHelo.createCASTask(1, false) + tasks[1] = casTask + local oTask = milHelo.createOrbitTask(2, 3600, theZone) + tasks[2] = oTask + WP.task.params.tasks = tasks + WP.type = "Turning Point" + + WP.x = targetPoint.x + WP.y = targetPoint.z + WP.ETA = 0 + WP.ETA_locked = false + WP.speed_locked = true + WP.formation_template = "" + return WP +end + +function milHelo.spawnForZone(theZone, targetZone) + local theRawData = dcsCommon.getNthItem(theZone.myGroups, 1) + local gData = dcsCommon.clone(theRawData) +--[[-- + -- pre-process gData: names, id etc + gData.name = dcsCommon.uuid(gData.name) + for idx, uData in pairs(gData.units) do + uData.name = dcsCommon.uuid(uData.name) + end + gData.groupId = nil + + -- change task according to missionType in Zone + gData.task = "CAS" + + -- create and process route + local route = {} + route.points = {} +-- gData.route = route + -- create take-off waypoint + local wpTOff = milHelo.createTakeOffWP(theZone) + -- depending on mission, create an orbit or land WP + local dest = targetZone:getPoint() + local wpDest = milHelo.createOrbitWP(theZone, dest) + -- move group to WP1 and add WP1 and WP2 to route +-- dcsCommon.moveGroupDataTo(theGroup, +-- fromWP.x, +-- fromWP.y) + +---- + dcsCommon.addRoutePointForGroupData(gData, wpTOff) + dcsCommon.addRoutePointForGroupData(gData, wpDest) +--]]-- + dcsCommon.dumpVar2Str("route", gData.route) + + -- make it a cty + if theZone.coa == 0 then + trigger.action.outText("+++milH: WARNING - zone <" .. theZone.name .. "> is NEUTRAL", 30) + end + local cty = dcsCommon.getACountryForCoalition(theZone.coa) + -- spawn + local groupCat = Group.Category.HELICOPTER + local theSpawnedGroup = coalition.addGroup(cty, groupCat, gData) + + return theSpawnedGroup, gData +end +-- +-- update and event +-- +function milHelo.update() + timer.scheduleFunction(milHelo.update, {}, timer.getTime() + 1) +end + +function milHelo.onEvent(theEvent) + +end + +-- +-- Config & start +-- +function milHelo.readConfigZone() + local theZone = cfxZones.getZoneByName("milHeloConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("milHeloConfig") + end + milHelo.verbose = theZone.verbose +end + + +function milHelo.start() +-- lib check + if not dcsCommon.libCheck then + trigger.action.outText("cfx civ helo requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx mil helo", milHelo.requiredLibs) then + return false + end + + -- read config + milHelo.readConfigZone() + + -- process milHelo Zones + local attrZones = cfxZones.getZonesWithAttributeNamed("milHelo") + for k, aZone in pairs(attrZones) do + milHelo.readMilHeloZone(aZone) -- process attributes + milHelo.addMilHeloZone(aZone) -- add to list + end + + attrZones = cfxZones.getZonesWithAttributeNamed("milTarget") + for k, aZone in pairs(attrZones) do + milHelo.readMilTargetZone(aZone) -- process attributes + milHelo.addMilTargetZone(aZone) -- add to list + end + + -- start update in 5 seconds + timer.scheduleFunction(milHelo.update, {}, timer.getTime() + 1/milHelo.ups) + + -- install event handler + world.addEventHandler(milHelo) + + -- say hi + trigger.action.outText("milHelo v" .. milHelo.version .. " started.", 30) + return true +end + +if not milHelo.start() then + trigger.action.outText("milHelo failed to start.", 30) + milHelo = nil +end + +-- do some one-time stuff +local theZone = dcsCommon.getFirstItem(milHelo.zones) +local targetZone = dcsCommon.getFirstItem(milHelo.targets) +milHelo.spawnForZone(theZone, targetZone) diff --git a/modules/unGrief.lua b/modules/unGrief.lua index d582162..b9cd3cb 100644 --- a/modules/unGrief.lua +++ b/modules/unGrief.lua @@ -1,5 +1,5 @@ unGrief = {} -unGrief.version = "1.2.0" +unGrief.version = "2.0.0" unGrief.verbose = false unGrief.ups = 1 unGrief.requiredLibs = { @@ -21,6 +21,11 @@ unGrief.disabledFlagValue = unGrief.enabledFlagValue + 100 -- DO NOT CHANGE - strict rules - warnings on enter/exit - warnings optional + 2.0.0 - dmlZones + - also trigger on birth event, more wrathful + - auto-turn on ssb when retaliation is SSB + - re-open slot after kick in 15 seconds + --]]-- @@ -55,13 +60,20 @@ function unGrief.createPvpWithZone(theZone) trigger.action.outText("+++uGrf: <" .. theZone.name .. "> is designated as PVP legal", 30) end - theZone.strictPVP = cfxZones.getBoolFromZoneProperty(theZone, "strict", false) + theZone.strictPVP = theZone:getBoolFromZoneProperty("strict", false) end -- vengeance: if player killed before, they are no longer welcome +function unGrief.reconcile(groupName) + -- re-open slot after player was kicked + trigger.action.setUserFlag(groupName, unGrief.enabledFlagValue) + trigger.action.outText("Group <" .. groupName .. "> now available again after pest control action", 30) +end + function unGrief.exactVengance(theEvent) - if theEvent.id == 20 then -- S_EVENT_PLAYER_ENTER_UNIT + if theEvent.id == 20 or -- S_EVENT_PLAYER_ENTER_UNIT + theEvent.id == 15 then -- Birth if not theEvent.initiator then return end local theUnit = theEvent.initiator if not theUnit.getPlayerName then return end -- wierd stuff happening here @@ -94,6 +106,7 @@ function unGrief.exactVengance(theEvent) -- tell ssb to kick now: trigger.action.setUserFlag(groupName, unGrief.disabledFlagValue) trigger.action.outText("Player <" .. playerName .. "> is not welcome here. Shoo! Shoo!", 30) + timer.scheduleFunction(unGrief.reconcile, groupName, timer.getTime() + 15) return end @@ -200,6 +213,7 @@ function unGrief:onEvent(theEvent) local groupName = theGroup:getName() -- tell ssb to kick now: trigger.action.setUserFlag(groupName, unGrief.disabledFlagValue) + timer.scheduleFunction(unGrief.reconcile, groupName, timer.getTime() + 15) return end -- aaand all your base are belong to us! @@ -256,23 +270,27 @@ function unGrief.readConfigZone() theZone = cfxZone.createSimpleZone("unGriefConfig") end - unGrief.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + unGrief.verbose = theZone.verbose - unGrief.graceKills = cfxZones.getNumberFromZoneProperty(theZone, "graceKills", 1) - unGrief.retaliation = cfxZones.getStringFromZoneProperty(theZone, "retaliation", "boom") -- other possible methods: ssb + unGrief.graceKills = theZone:getNumberFromZoneProperty("graceKills", 1) + unGrief.retaliation = theZone:getStringFromZoneProperty("retaliation", "boom") -- other possible methods: ssb unGrief.retaliation = dcsCommon.trim(unGrief.retaliation:lower()) - - - unGrief.wrathful = cfxZones.getBoolFromZoneProperty(theZone, "wrathful", false) - - unGrief.pve = cfxZones.getBoolFromZoneProperty(theZone, "pve", false) - if cfxZones.hasProperty(theZone, "pveOnly") then - unGrief.pve = cfxZones.getBoolFromZoneProperty(theZone, "pveOnly", false) + if unGrief.retaliation == "ssb" then + -- now turn on ssb + trigger.action.setUserFlag("SSB",100) + trigger.action.outText("unGrief: SSB enabled for retaliation.", 30) end - unGrief.ignoreAI = cfxZones.getBoolFromZoneProperty(theZone, "ignoreAI", false) + unGrief.wrathful = theZone:getBoolFromZoneProperty("wrathful", false) - unGrief.PVPwarnings = cfxZones.getBoolFromZoneProperty(theZone, "warnings", true) + unGrief.pve = theZone:getBoolFromZoneProperty("pve", false) + if theZone:hasProperty("pveOnly") then + unGrief.pve = theZone:getBoolFromZoneProperty("pveOnly", false) + end + + unGrief.ignoreAI = theZone:getBoolFromZoneProperty("ignoreAI", false) + + unGrief.PVPwarnings = theZone:getBoolFromZoneProperty("warnings", true) if unGrief.verbose then trigger.action.outText("+++uGrf: read config", 30) diff --git a/tutorial & demo missions/demo - boom boom.miz b/tutorial & demo missions/demo - boom boom.miz index 76bcf8f..efd5597 100644 Binary files a/tutorial & demo missions/demo - boom boom.miz and b/tutorial & demo missions/demo - boom boom.miz differ