diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 1612a74..fff7e3e 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 ecf1890..8aae624 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/autoCSAR.lua b/modules/autoCSAR.lua index f23a351..a856961 100644 --- a/modules/autoCSAR.lua +++ b/modules/autoCSAR.lua @@ -1,14 +1,18 @@ autoCSAR = {} -autoCSAR.version = "1.0.0" +autoCSAR.version = "1.1.0" autoCSAR.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } autoCSAR.killDelay = 2 * 60 autoCSAR.counter = 31 -- any number is good, to kick-off counting +autoCSAR.trackedEjects = {} -- we start tracking on eject + --[[-- VERSION HISTORY 1.0.0 - Initial Version + 1.1.0 - allow open water CSAR, fake pilot with GRG Soldier + - can be disabled by seaCSAR = false --]]-- function autoCSAR.removeGuy(args) @@ -18,6 +22,13 @@ function autoCSAR.removeGuy(args) end end +function autoCSAR.isOverWater(theUnit) + local pPoint = theUnit:getPoint() + pPoint.y = pPoint.z -- make it getSurfaceType compatible + local surf = land.getSurfaceType(pPoint) + return surf == 2 or surf == 3 +end + function autoCSAR.createNewCSAR(theUnit) if not csarManager then trigger.action.outText("+++aCSAR: CSAR Manager not loaded, aborting", 30) @@ -40,25 +51,120 @@ function autoCSAR.createNewCSAR(theUnit) -- for later expansion local theGroup = theUnit:getGroup() if theGroup then - trigger.action.outText("We have a group for <" .. theUnit:getName() .. ">", 30) + -- now happens for faked sea CSAR units + --trigger.action.outText("We have a group for <" .. theUnit:getName() .. ">", 30) end + -- now, if theUnit is over open water, this will be killed instantly + -- and must therefore be replaced with a stand-in + local pPoint = theUnit:getPoint() + pPoint.y = pPoint.z -- make it getSurfaceType compatible + local surf = land.getSurfaceType(pPoint) + local splashdown = false + + if surf == 2 or surf == 3 then + trigger.action.outTextForCoalition(coa, "Parachute splashdown over open water reported!", 30) + splashdown = true + -- create a replacement unit since pilot will be killed + local theBoyGroup = dcsCommon.createSingleUnitGroup( + "Xray-" .. autoCSAR.counter, + "Soldier M4 GRG", -- "Soldier M4 GRG", + pPoint.x, + pPoint.z, + 0) + local theSideCJTF = dcsCommon.coalition2county(coa) -- get the correct county CJTF + local theGroup = coalition.addGroup(theSideCJTF, Group.Category.GROUND, theBoyGroup) + -- now access replacement unit + local allUnits = theGroup:getUnits() + theUnit = allUnits[1] -- get first (and only) unit + end -- create a CSAR mission now csarManager.createCSARForParachutist(theUnit, "Xray-" .. autoCSAR.counter) autoCSAR.counter = autoCSAR.counter + 1 - -- schedule removal of pilot + -- schedule removal of pilot local args = {} - args.theGuy = theUnit - timer.scheduleFunction(autoCSAR.removeGuy, args, timer.getTime() + autoCSAR.killDelay) + args.theGuy = theUnit + if splashdown then + timer.scheduleFunction(autoCSAR.removeGuy, args, timer.getTime() + 1) -- in one second + else + timer.scheduleFunction(autoCSAR.removeGuy, args, timer.getTime() + autoCSAR.killDelay) + end end function autoCSAR:onEvent(event) - if event.id == 31 then -- landing_after_eject +-- trigger.action.outText("autoCSAR: event = " .. event.id, 30) + if event.id == 31 then -- landing_after_eject, does not happen at sea + -- to prevent double invocations for same process + -- check that we are still tracking this ejection if event.initiator then - autoCSAR.createNewCSAR(event.initiator) + local uid = tonumber(event.initiator:getID()) + if autoCSAR.trackedEjects[uid] then + trigger.action.outText("aCSAR: filtered double sea csar (player) event for uid = <" .. uid .. ">", 30) + autoCSAR.trackedEjects[uid] = nil -- reset + return + end + autoCSAR.createNewCSAR(event.initiator) +-- autoCSAR.trackedEjects[event.initiator] = nil +-- trigger.action.outText("autocsar: LAE for " .. autoCSAR.trackedEjects[event.initiator], 30) +-- else +-- trigger.action.outText("autoCSAR: ignored LAE event", 30) +-- end end end + + if event.id == 6 and autoCSAR.seaCSAR then -- eject, start tracking + if event.initiator then + -- see if this happened over open water and immediately + -- create a seaCSAR + + --local uid = tonumber(event.initiator:getID()) +-- trigger.action.outText("autoCSAR: started tracking - chair + pilot", 30) +-- autoCSAR.trackedEjects[event.initiator] = "chair+pilot" -- start with this + if autoCSAR.isOverWater(event.initiator) then + --trigger.action.outText("attempting to walk on water", 30) + autoCSAR.createNewCSAR(event.initiator) + end + + -- also mark this one as completed + local uid = tonumber(event.initiator:getID()) + autoCSAR.trackedEjects[uid] = "processed" + end + end + +--[[-- + if event.id == 33 then -- separate chair from pilot + if event.initiator then + --local uid = tonumber(event.initiator:getID()) + --local pid = tonumber(event.target:getID()) + --if uid == 0 then + --trigger.action.outText("uid = 0, abort tracking", 30) + --return + --end + trigger.action.outText("autoCSAR: track change from seat to pilot <" .. event.target:getName() .. ">", 30) + autoCSAR.trackedEjects[event.initiator] = "chair only" + autoCSAR.trackedEjects[event.target] = "pilot" + end + end + + if event.id == 9 then -- pilot dead + if event.initiator then + --local uid = tonumber(event.initiator:getID()) + trigger.action.outText("autoCSAR: pilot id=xxx dead", 30) + if autoCSAR.trackedEjects[event.initiator] then + trigger.action.outText("confirm tracked pilot dead after ejection", 30) + if autoCSAR.isOverWater(event.initiator) then + trigger.action.outText("attempt to walk on water", 30) + autoCSAR.createNewCSAR(event.initiator) + end + autoCSAR.trackedEjects[event.initiator] = nil + end + else + trigger.action.outText("autoCSAR - no initiator for zed", 30) + end + end +--]]-- + end function autoCSAR.readConfigZone() @@ -80,6 +186,8 @@ function autoCSAR.readConfigZone() autoCSAR.blueCSAR = cfxZones.getBoolFromZoneProperty(theZone, "blueCSAR", true) end + autoCSAR.seaCSAR = cfxZones.getBoolFromZoneProperty(theZone, "seaCSAR", true) + if autoCSAR.verbose then trigger.action.outText("+++aCSAR: read config", 30) end diff --git a/modules/cfxReconMode.lua b/modules/cfxReconMode.lua index fc3177c..992739b 100644 --- a/modules/cfxReconMode.lua +++ b/modules/cfxReconMode.lua @@ -1,5 +1,5 @@ cfxReconMode = {} -cfxReconMode.version = "2.1.3" +cfxReconMode.version = "2.1.4" cfxReconMode.verbose = false -- set to true for debug info cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered @@ -84,6 +84,8 @@ VERSION HISTORY - wildcard in message format - fix for mgrs bug in message (zone coords, not unit) 2.1.3 - added cfxReconMode.name to allow direct acces with test zone flag + 2.1.4 - canDetect() also checks if unit has been activated + canDetect has strenghtened isExist() guard cfxReconMode is a script that allows units to perform reconnaissance missions and, after detecting units, marks them on the map with @@ -319,7 +321,7 @@ function cfxReconMode.canDetect(scoutPos, theGroup, visRange) -- returns true and pos when detected local allUnits = theGroup:getUnits() for idx, aUnit in pairs(allUnits) do - if aUnit:isExist() and aUnit:getLife() >= 1 then + if Unit.isExist(aUnit) and aUnit:isActive() and aUnit:getLife() >= 1 then local uPos = aUnit:getPoint() uPos.y = uPos.y + 3 -- raise my 3 meters local d = dcsCommon.distFlat(scoutPos, uPos) diff --git a/modules/cfxSmokeZones.lua b/modules/cfxSmokeZones.lua index 2f98073..c1d5870 100644 --- a/modules/cfxSmokeZones.lua +++ b/modules/cfxSmokeZones.lua @@ -1,5 +1,5 @@ cfxSmokeZone = {} -cfxSmokeZone.version = "1.1.1" +cfxSmokeZone.version = "1.1.2" cfxSmokeZone.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -17,6 +17,7 @@ cfxSmokeZone.requiredLibs = { - random color support 1.1.0 - Watchflag upgrade 1.1.1 - stopSmoke? input + 1.1.2 - 'agl', 'alt' synonymous for altitude to keep in line with fireFX --]]-- cfxSmokeZone.smokeZones = {} @@ -36,6 +37,11 @@ function cfxSmokeZone.processSmokeZone(aZone) aZone.smokeColor = theColor aZone.smokeAlt = cfxZones.getNumberFromZoneProperty(aZone, "altitude", 1) + if cfxZones.hasProperty(aZone, "alt") then + aZone.smokeAlt = cfxZones.getNumberFromZoneProperty(aZone, "alt", 1) + elseif cfxZones.hasProperty(aZone, "agl") then + aZone.smokeAlt = cfxZones.getNumberFromZoneProperty(aZone, "agl", 1) + end -- paused aZone.paused = cfxZones.getBoolFromZoneProperty(aZone, "paused", false) diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 05c9b68..e305b39 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "3.0.3" +cfxZones.version = "3.0.6" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -122,7 +122,8 @@ cfxZones.version = "3.0.3" - 3.0.3 - new getLinkedUnit() - 3.0.4 - new createRandomPointOnZoneBoundary() - 3.0.5 - getPositiveRangeFromZoneProperty() now also supports upper bound (optional) - +- 3.0.6 - new createSimplePolyZone() + - new createSimpleQuadZone() --]]-- cfxZones.verbose = false @@ -366,7 +367,12 @@ function cfxZones.copyPoint(inPoint) local newPoint = {} newPoint.x = inPoint.x newPoint.y = inPoint.y - newPoint.z = inPoint.z + -- handle xz only + if inPoint.z then + newPoint.z = inPoint.z + else + newPoint.z = inPoint.y + end return newPoint end @@ -536,6 +542,54 @@ function cfxZones.createCircleZone(name, x, z, radius) return newZone end +function cfxZones.createSimplePolyZone(name, location, points, addToManaged) + if not addToManaged then addToManaged = false end + if not location then + location = {} + end + if not location.x then location.x = 0 end + if not location.z then location.z = 0 end + + local newZone = cfxZones.createPolyZone(name, points) + + if addToManaged then + cfxZones.addZoneToManagedZones(newZone) + end + return newZone +end + +function cfxZones.createSimpleQuadZone(name, location, points, addToManaged) + if not location then + location = {} + end + if not location.x then location.x = 0 end + if not location.z then location.z = 0 end + + -- synthesize 4 points if they don't exist + -- remember: in DCS positive x is up, positive z is right + if not points then + points = {} + end + if not points[1] then + -- upper left + points[1] = {x = location.x-1, y = 0, z = location.z-1} + end + if not points[2] then + -- upper right + points[2] = {x = location.x-1, y = 0, z = location.z+1} + end + if not points[3] then + -- lower right + points[3] = {x = location.x+1, y = 0, z = location.z+1} + end + if not points[4] then + -- lower left + points[4] = {x = location.x+1, y = 0, z = location.z-1} + end + + return cfxZones.createSimplePolyZone(name, location, points, addToManaged) +end + function cfxZones.createPolyZone(name, poly) -- poly must be array of point type local newZone = {} newZone.isCircle = false diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index d57fb3f..3d1061f 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -1,5 +1,5 @@ csarManager = {} -csarManager.version = "2.2.0" +csarManager.version = "2.2.3" csarManager.verbose = false csarManager.ups = 1 @@ -50,9 +50,19 @@ csarManager.ups = 1 - score global and per-mission - isCSARTarget API - 2.2.1 - added troopCarriers attribute to config - - passes own troop carriers to dcsCommin.isTroopCarrier() + - passes own troop carriers to dcsCommon.isTroopCarrier() + - 2.2.2 - enable CSAR missions in water + - csar name defaults to zone name + - better randomization of pilot's point in csar mission, + supports quad zone + - 2.2.3 - better support for red/blue + - allow neutral pick-up + - directions to closest safe zone + - CSARBASE attribute now carries coalition + - deprecated coalition attribute + + INTEGRATES AUTOMATICALLY WITH playerScore IF INSTALLED - --]]-- -- modules that need to be loaded BEFORE I run csarManager.requiredLibs = { @@ -61,15 +71,14 @@ csarManager.requiredLibs = { "cfxPlayer", -- player monitoring and group monitoring "nameStats", -- generic data module for weight "cargoSuper", --- "cfxCommander", -- needed if you want to hand-create CSAR missions +-- "cfxCommander", -- needed only if you want to hand-create CSAR missions } --- integrates automatically with playerScore if installed -- -- OPTIONS -- -csarManager.useSmoke = true -- smoke is a performance killer, so you can turn it off +csarManager.useSmoke = true csarManager.smokeColor = 4 -- when using smoke @@ -82,7 +91,7 @@ csarManager.myEvents = {3, 4, 5} -- 3 = take off, 4 = land, 5 = crash -- CASR MISSION -- csarManager.openMissions = {} -- all currently available missions -csarManager.csarBases = {} -- all bases where we can drop off rescued pilots +csarManager.csarBases = {} -- all bases where we can drop off rescued pilots. NOT a zone! csarManager.csarZones = {} -- zones for spawning csarManager.missionID = 1 -- to create uuid @@ -113,6 +122,10 @@ function csarManager.createDownedPilot(theMission, existingUnit) local aLocation = {} local aHeading = 0 -- in rads local newTargetZone = theMission.zone + -- if mission.radius is < 1 we do not randomize location + -- else location is somewhere in the middle of zone + -- csar mission zones randomize by themselves, and pass + -- a radius of <1, this is only for ejecting pilots if newTargetZone.radius > 1 then aLocation, aHeading = dcsCommon.randomPointOnPerimeter(newTargetZone.radius / 2 + 3, newTargetZone.point.x, newTargetZone.point.z) else @@ -546,8 +559,6 @@ function csarManager.heloDeparted(theUnit) local theGroup = theUnit:getGroup() conf.id = theGroup:getID() conf.currentState = 1 -- in the air - - end -- @@ -555,7 +566,6 @@ end -- Helo Crashed -- -- - function csarManager.heloCrashed(theUnit) if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end -- problem: this isn't called on network games. @@ -698,6 +708,16 @@ function csarManager.setCommsMenu(theUnit) {conf, "unload one"} ) table.insert(conf.myCommands, theCommand) + + commandTxt = "Direction to nearest safe zone" + theCommand = missionCommands.addCommandForGroup( + conf.id, + commandTxt, + conf.myMainMenu, + csarManager.redirectDirections, + {conf, "redirect"} + ) + table.insert(conf.myCommands, theCommand) end @@ -705,19 +725,32 @@ function csarManager.redirectListCSARRequests(args) timer.scheduleFunction(csarManager.doListCSARRequests, args, timer.getTime() + 0.1) end +function csarManager.openMissionsForSide(theSide) + local theMissions = {} + for idx, aMission in pairs(csarManager.openMissions) do + if aMission.side == theSide or aMission.side == 0 then + table.insert(theMissions, aMission) + end + end + return theMissions +end + function csarManager.doListCSARRequests(args) local conf = args[1] local param = args[2] local theUnit = conf.unit local point = theUnit:getPoint() + local theSide = theUnit:getCoalition() --trigger.action.outText("+++csar: ".. theUnit:getName() .." issued csar status request", 30) local report = "\nCrews requesting evacuation\n" - if #csarManager.openMissions < 1 then + local openMissions = csarManager.openMissionsForSide(theSide) + + if #openMissions < 1 then report = report .. "\nNo requests, all crew are safe." else -- iterate through all troops onboard to get their status - for idx, mission in pairs(csarManager.openMissions) do + for idx, mission in pairs(openMissions) do local d = dcsCommon.distFlat(point, mission.zone.point) * 0.000539957 d = math.floor(d * 10) / 10 local b = dcsCommon.bearingInDegreesFromAtoB(point, mission.zone.point) @@ -730,9 +763,9 @@ function csarManager.doListCSARRequests(args) end end end - - if #csarManager.csarBases < 1 then - report = report .. "\n\nWARNING: NO CSAR BASES TO DELIVER EVACUEES" + local myBases = csarManager.getCSARBasesForSide(theSide) + if #myBases < 1 then + report = report .. "\n\nWARNING: NO CSAR BASES TO DELIVER EVACUEES TO" end report = report .. "\n" @@ -818,6 +851,59 @@ function csarManager.unloadOne(args) end end + +function csarManager.redirectDirections(args) + timer.scheduleFunction(csarManager.directions, args, timer.getTime() + 0.1) +end + +function csarManager.directions(args) + local conf = args[1] + local param = args[2] + local theUnit = conf.unit + local myName = theUnit:getName() + local theSide = theUnit:getCoalition() + local report = "Nothing to report." + + -- get all safe zones + local myBases = csarManager.getCSARBasesForSide(theSide) + if #myBases < 1 then + report = "\n\nWARNING: NO CSAR BASES TO DELIVER EVACUEES TO" + else + -- find nearest zone + local p = theUnit:getPoint() + p.y = 0 + local dMin = math.huge + local theBase = nil + local dP = nil + for idx, aBase in pairs(myBases) do + local z = aBase.zone + local zp = cfxZones.getPoint(z) + zp.y = 0 + local d = dcsCommon.dist(p, zp) + if d < dMin then + theBase = aBase + dP = zp + dMin = d + end + end + + -- see if we are inside + if cfxZones.isPointInsideZone(p, theBase.zone) then + report = "\nYou are inside safe zone " .. theBase.name .. "." + else + -- get bearing and distance + dMin = dMin / 1000 * 0.539957 -- in nm + + dMin = math.floor(dMin * 10) / 10 + local bearing = dcsCommon.bearingInDegreesFromAtoB(p, dP) + report = "\nClosest safe zone for " .. myName .. " is " .. theBase.name ..", bearing " .. bearing .. " at " .. dMin .. "nm" + end + end + + report = report .. "\n" + trigger.action.outTextForGroup(conf.id, report, 30) + trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) +end -- -- Player event callbacks -- @@ -870,14 +956,23 @@ end function csarManager.addCSARBase(aZone) local csarBase = {} csarBase.zone = aZone + +--[[-- local bName = cfxZones.getStringFromZoneProperty(aZone, "CSARBASE", "XXX") if bName == "XXX" then bName = aZone.name end csarBase.name = cfxZones.getStringFromZoneProperty(aZone, "name", bName) +--]]-- + -- CSARBASE now carries the coalition in the CSARBASE attribute + csarBase.side = cfxZones.getCoalitionFromZoneProperty(aZone, "CSARBASE", 0) + -- backward-compatibility to older versions. + -- will be deprecated + if cfxZones.hasProperty(aZone, "coalition") then + csarBase.side = cfxZones.getCoalitionFromZoneProperty(aZone, "CSARBASE", 0) + end - -- read further properties like facilities that may - -- need to be matched - csarBase.side = cfxZones.getCoalitionFromZoneProperty(aZone, "coalition", 0) - + -- see if we have provided a name field, default zone name + csarBase.name = cfxZones.getStringFromZoneProperty(aZone, "name", aZone.name) + table.insert(csarManager.csarBases, csarBase) if csarManager.verbose or aZone.verbose then @@ -894,6 +989,15 @@ function csarManager.getCSARBaseforZone(aZone) return nil end +function csarManager.getCSARBasesForSide(theSide) + local bases = {} + for idx, aBase in pairs(csarManager.csarBases) do + if aBase.side == 0 or aBase.side == theSide then + table.insert(bases, aBase) + end + end + return bases +end -- -- -- U P D A T E @@ -947,7 +1051,8 @@ function csarManager.update() -- every second for idx, csarMission in pairs (csarManager.openMissions) do -- check if we are inside trigger range on the same side local d = dcsCommon.distFlat(uPoint, csarMission.zone.point) - if (uSide == csarMission.side) and (d < csarManager.rescueTriggerRange) then + if ((uSide == csarMission.side) or (csarMission.side == 0) ) + and (d < csarManager.rescueTriggerRange) then -- we are in trigger distance. if we did not notify before -- do it now if not dcsCommon.arrayContainsString(csarMission.messagedUnits, uName) then @@ -1064,15 +1169,18 @@ function csarManager.update() -- every second -- this should always be true, but you never know local currVal = cfxZones.getFlagValue(theZone.startCSAR, theZone) if currVal ~= theZone.lastCSARVal then + -- set up random point in zone + local mPoint = cfxZones.createRandomPointInZone(theZone) local theMission = csarManager.createCSARMissionData( - cfxZones.getPoint(theZone), - theZone.csarSide, - theZone.csarFreq, - theZone.csarName, - theZone.numCrew, - theZone.timeLimit, - theZone.csarMapMarker, - theZone.radius) + mPoint, --cfxZones.getPoint(theZone), -- point + theZone.csarSide, -- theSide + theZone.csarFreq, -- freq + theZone.csarName, -- name + theZone.numCrew, -- numCrew + theZone.timeLimit, -- timeLimit + theZone.csarMapMarker, -- mapMarker + 0.1, --theZone.radius) -- radius + nil) -- parashoo unit csarManager.addMission(theMission) theZone.lastCSARVal = currVal if csarManager.verbose then @@ -1097,9 +1205,12 @@ function csarManager.createCSARforUnit(theUnit, pilotName, radius, silent, score local csarPoint = dcsCommon.randomPointInCircle(radius, radius/2, point.x, point.z) -- check the ground- water will kill the pilot + -- not any more! pilot can float + csarPoint.y = csarPoint.z local surf = land.getSurfaceType(csarPoint) + --[[-- if surf == 2 or surf == 3 then if not silent then trigger.action.outTextForCoalition(coal, "Bad chute! Bad chute! ".. pilotName .. " did not survive ejection out of their " .. theUnit:getTypeName(), 30) @@ -1107,18 +1218,19 @@ function csarManager.createCSARforUnit(theUnit, pilotName, radius, silent, score end return end + --]]-- csarPoint.y = land.getHeight(csarPoint) -- when we get here, the terrain is ok, so let's drop the pilot local theMission = csarManager.createCSARMissionData( - csarPoint, - coal, - nil, - pilotName, - 1, - nil, - nil) + csarPoint, -- point + coal, -- side + nil, -- freq + pilotName, -- name + 1, -- num crew + nil, -- time limit + nil) -- map mark, inRadius, parashooUnit theMission.score = score csarManager.addMission(theMission) if not silent then @@ -1130,13 +1242,13 @@ end function csarManager.createCSARForParachutist(theUnit, name) -- invoked with parachute guy on ground as theUnit local coa = theUnit:getCoalition() local pos = theUnit:getPoint() - -- unit DOES NOT HAVE GROUP!!! + -- unit DOES NOT HAVE GROUP!!! (unless water splashdown) -- create a CSAR mission now local theMission = csarManager.createCSARMissionData(pos, coa, nil, name, nil, nil, nil, 0.1, nil) csarManager.addMission(theMission) -- if not silent then - trigger.action.outTextForCoalition(coa, "MAYDAY MAYDAY MAYDAY! ".. name .. " requesting extraction after eject!", 30) - trigger.action.outSoundForGroup(coa, csarManager.actionSound) -- "Quest Snare 3.wav") + trigger.action.outTextForCoalition(coa, "MAYDAY MAYDAY MAYDAY! ".. name .. " requesting extraction after eject!", 30) + trigger.action.outSoundForGroup(coa, csarManager.actionSound) -- "Quest Snare 3.wav") -- end end @@ -1164,6 +1276,7 @@ function csarManager.readCSARZone(theZone) local theSide = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0) theZone.csarSide = theSide theZone.csarName = cfxZones.getStringFromZoneProperty(theZone, "name", "") + theZone.csarName = theZone.name -- default CSAR Name if cfxZones.hasProperty(theZone, "csarName") then theZone.csarName = cfxZones.getStringFromZoneProperty(theZone, "csarName", "") end @@ -1199,15 +1312,17 @@ function csarManager.readCSARZone(theZone) end if (not deferred) then + local mPoint = cfxZones.createRandomPointInZone(theZone) local theMission = csarManager.createCSARMissionData( - theZone.point, + mPoint, theZone.csarSide, theZone.csarFreq, theZone.csarName, theZone.numCrew, theZone.timeLimit, theZone.csarMapMarker, - theZone.radius) + 0.1, -- theZone.radius, + nil) -- parashoo unit csarManager.addMission(theMission) end @@ -1385,4 +1500,8 @@ end - when unloading one by menu, update weight!!! -- allow neutral pick-up + + -- allow any airfied to be csarsafe by default, no longer requires csarbase + -- get vector to closes csarbase + --]]-- \ No newline at end of file diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 1c2e75b..17e0751 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.8.3" +dcsCommon.version = "2.8.4" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -140,12 +140,17 @@ dcsCommon.version = "2.8.3" - isTroopCarrierType uses wildArrayContainsString 2.8.3 - small optimizations in bearingFromAtoB() - new whichSideOfMine() + 2.8.4 - new rotatePointAroundOriginRad() + - new rotatePointAroundPointDeg() + - new rotatePointAroundPointRad() + - getClosestAirbaseTo() now supports passing list of air bases + --]]-- -- dcsCommon is a library of common lua functions -- for easy access and simple mission programming - -- (c) 2021, 2022 by Chritian Franz and cf/x AG + -- (c) 2021 - 2023 by Chritian Franz and cf/x AG dcsCommon.verbose = false -- set to true to see debug messages. Lots of them dcsCommon.uuidStr = "uuid-" @@ -506,9 +511,12 @@ dcsCommon.version = "2.8.3" return nil end - function dcsCommon.getClosestAirbaseTo(thePoint, filterCat, filterCoalition) + function dcsCommon.getClosestAirbaseTo(thePoint, filterCat, filterCoalition, allYourBase) local delta = math.huge - local allYourBase = dcsCommon.getAirbasesWhoseNameContains("*", filterCat, filterCoalition) -- get em all and filter + if not allYourBase then + allYourBase = dcsCommon.getAirbasesWhoseNameContains("*", filterCat, filterCoalition) -- get em all and filter + end + local closestBase = nil for idx, aBase in pairs(allYourBase) do -- iterate them all @@ -1857,9 +1865,7 @@ dcsCommon.version = "2.8.3" end; - function dcsCommon.rotatePointAroundOrigin(inX, inY, angle) -- angle in degrees - local rads = 3.14152 / 180 -- convert to radiants. - angle = angle * rads -- turns into rads + function dcsCommon.rotatePointAroundOriginRad(inX, inY, angle) -- angle in degrees local c = math.cos(angle) local s = math.sin(angle) local px @@ -1868,6 +1874,38 @@ dcsCommon.version = "2.8.3" py = inX * s + inY * c return px, py end + + function dcsCommon.rotatePointAroundOrigin(inX, inY, angle) -- angle in degrees + local rads = 3.14152 / 180 -- convert to radiants. + angle = angle * rads -- turns into rads + local px, py = dcsCommon.rotatePointAroundOriginRad(inX, inY, angle) + return px, py + end + + function dcsCommon.rotatePointAroundPointRad(x, y, px, py, angle) + x = x - px + y = y - py + x, y = dcsCommon.rotatePointAroundOriginRad(x, y, angle) + x = x + px + y = y + py + return x, y + end + + function dcsCommon.rotatePointAroundPointDeg(x, y, px, py, degrees) + x, y = dcsCommon.rotatePointAroundPointRad(x, y, px, py, degrees * 3.14152 / 180) + return x, y + end + + -- rotates a Vec3-base inPoly on XZ pane around inPoint on XZ pane + function dcsCommon.rotatePoly3AroundVec3Rad(inPoly, inPoint, rads) + local outPoly = {} + for idx, aVertex in pairs(inPoly) do + local x, z = dcsCommon.rotatePointAroundPointRad(aVertex.x, aVertex.z, inPoint.x, inPoint.z, rads) + local v3 = {x = x, y = aVertex.y, z = z} + outPoly[idx] = v3 + end + return outPoly + end function dcsCommon.rotateUnitData(theUnit, degrees, cx, cz) if not cx then cx = 0 end diff --git a/modules/fireFX.lua b/modules/fireFX.lua index e039a91..0e690ae 100644 --- a/modules/fireFX.lua +++ b/modules/fireFX.lua @@ -12,6 +12,7 @@ fireFX.fx = {} Version History 1.0.0 - Initial version 1.1.0 - persistence + 1.1.1 - agl attribute --]]-- @@ -60,6 +61,9 @@ function fireFX.createFXWithZone(theZone) theZone.density = cfxZones.getNumberFromZoneProperty(theZone, "density", 0.5) + theZone.agl = cfxZones.getNumberFromZoneProperty(theZone, "AGL", 0) + + if cfxZones.hasProperty(theZone, "start?") then theZone.fxStart = cfxZones.getStringFromZoneProperty(theZone, "start?", "*") theZone.fxLastStart = cfxZones.getFlagValue(theZone.fxStart, theZone) @@ -96,7 +100,7 @@ end function fireFX.startTheFire(theZone) if not theZone.burning then local p = cfxZones.getPoint(theZone) - p.y = land.getHeight({x = p.x, y = p.z}) + p.y = land.getHeight({x = p.x, y = p.z}) + theZone.agl local preset = theZone.fxCode local density = theZone.density trigger.action.effectSmokeBig(p, preset, density, theZone.name) diff --git a/modules/taxiPolice.lua b/modules/taxiPolice.lua new file mode 100644 index 0000000..7f74593 --- /dev/null +++ b/modules/taxiPolice.lua @@ -0,0 +1,280 @@ +taxiPolice = {} +taxiPolice.version = "0.0.0" +taxiPolice.verbose = true +taxiPolice.ups = 1 -- checks per second +taxiPolice.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course +} +--[[-- +-- ensure that a player doesn't overspeed on taxiways. uses speedLimit and violateDuration to determine if to fine + +-- create runway polys here: https://wiki.hoggitworld.com/view/DCS_func_getRunways + +-- works as follows: +-- when a player's plane is not inAir, they are monitored. +-- when on a runway or too far from airfield (only airfields are monitored) monitoring ends +-- when monitored and overspeeding, they first receive a warning, and after n warnings they receive retribution +--]]-- +taxiPolice.speedLimit = 14 -- m/s . 14 m/s = 50 km/h, 10 m/s = 36 kmh +taxiPolice.triggerTime = 3 -- seconds until we register a speeding violation +taxiPolice.rwyLeeway = 5 -- meters on each side +taxiPolice.rwyExtend = 500 -- meters in front and at end +taxiPolice.airfieldMaxDist = 3000 -- radius around airfield in which we operate +taxiPolice.runways = {} -- indexed by airbase name, then by rwName +taxiPolice.suspects = {} -- units that are currently behaving naughty +taxiPolice.tickets = {} -- number of warnings per player +taxiPolice.maxTickets = 3 -- number of tickes without retribution + +function taxiPolice.buildRunways() + local bases = world.getAirbases() + local mId = 0 + for idb, aBase in pairs (bases) do -- i = 1, #base do + local name = aBase:getName() + local rny = aBase:getRunways() + if rny then + local runways = {} + for idx, rwy in pairs(rny) do -- j = 1, #rny do + -- calcualte quad that encloses taxiway + local points = {} -- quad + local init = rwy.position + local bearing = rwy.course * -1 -- "*-1 to make meaningful" + local rwName = bearing * 57.2958 -- rads to degree + if rwName < 0 then rwName = rwName + 360 end + rwName = math.floor(rwName / 10) + rwName = tostring(rwName) + -- calculate start and end point of RWY, "heading 0" + local radius = rwy.length/2 + taxiPolice.rwyExtend + local pStart = {y=0, x = init.x + radius, z = init.z } + local pEnd = {y=0, x = init.x - radius, z = init.z} + -- Build runway with width; at 0 heading (trivial case) + local width = rwy.width/2 + taxiPolice.rwyLeeway + local dz1 = width + local dz2 = - width + points[1] = {y = 0, x = pStart.x, z = pStart.z + dz1} + points[2] = {y = 0, x = pStart.x, z = pStart.z + dz2} + points[3] = {y = 0, x = pEnd.x, z = pEnd.z + dz2} + points[4] = {y = 0, x = pEnd.x, z = pEnd.z + dz1} + -- rotate RWY "0" to RW "bearing" + local poly = dcsCommon.rotatePoly3AroundVec3Rad(points, init, bearing) + + mId = mId + 1 + -- draw on map + if taxiPolice.verbose then + trigger.action.quadToAll(-1, mId, poly[1], poly[2], poly[3], poly[4], {0, 0, 0, 1}, {0, 0, 0, .5}, 3) + end + -- save runway under name + runways[rwName] = poly + + -- build a 100x100 quad to show base's center + end + + taxiPolice.runways[name] = runways + if taxiPolice.verbose then + local points = {} + local pStart = aBase:getPoint() + local pEnd = aBase:getPoint() + local dx1 = 100 + local dz1 = 100 + local dx2 = -100 + local dz2 = -100 + points[1] = {y = 0, x = pStart.x + dx1, z = pStart.z + dz1} + points[2] = {y = 0, x = pStart.x + dx2, z = pStart.z + dz1} + points[3] = {y = 0, x = pEnd.x + dx2, z = pEnd.z + dz2} + points[4] = {y = 0, x = pEnd.x + dx1, z = pEnd.z + dz2} + mId = mId + 1 + trigger.action.quadToAll(-1, mId, points[1], points[2], points[3], points[4], {1, 0, 0, 1}, {1, 0, 0, .5}, 3) + end + end + end +end + + +-- +-- Checking and Policing +-- +function taxiPolice.retributeAgainst(theUnit) + -- player did not learn. + local player = theUnit:getPlayerName() + trigger.action.outText("Player <" .. player .. "> behaves reckless and is being reprimanded", 30) + + -- do some harsh stuff + local pGrp = theUnit:getGroup() + local gID = pGrp:getID() + trigger.action.outTextForGroup(gID, "We don't appreciate your behavior. Stop it NOW. Here's something to think about...", 30) + + trigger.action.setUnitInternalCargo(theUnit:getName() , 1000000 ) -- add 1000t +end + +function taxiPolice.checkUnit(theUnit, allAirfields) + if not theUnit.getPlayerName then return end + local player = theUnit:getPlayerName() + if not player then return end + + local p = theUnit:getPoint() + p.y = 0 + local base, dist = dcsCommon.getClosestAirbaseTo(p, nil, nil, allAirfields) + if dist > taxiPolice.airfieldMaxDist then + taxiPolice.suspects[player] = nil -- remove watched status + return + end -- not interesting + + local vel = dcsCommon.getUnitSpeed(theUnit) + + -- if we get here, player is on the ground, in proximity to airfield + if vel < taxiPolice.speedLimit then + taxiPolice.suspects[player] = nil -- remove watched status + return + end -- not speeding + + -- if we get here, we also exceed the speed limit + -- check if we are on a runway + local myRunways = taxiPolice.runways[base:getName()] + if not myRunways then + -- this base is turned off + --trigger.action.outText("unable to find raunways for <" .. base:getName() .. ">", 30) + return + end + + for rwName, aRunway in pairs(myRunways) do + if cfxZones.isPointInsidePoly(p, aRunway) then + --trigger.action.outText("<" .. theUnit:getName() .. "> is on RWY <" .. rwName .. ">", 30) + taxiPolice.suspects[player] = nil -- remove watched status + return + end + end + + -- if we get here, player is speeding on airfield + local speedingSince = taxiPolice.suspects[player] -- time since speeding started + + if not speedingSince then + -- we start watching now. At least one second will be grace period + taxiPolice.suspects[player] = timer.getTime() + return + end + + if timer.getTime() - speedingSince < taxiPolice.triggerTime then + -- we are watching, but not acting + --trigger.action.outText(player .. ", you are being watched: <" .. timer.getTime() - speedingSince .. ">", 30) + return + end + + -- when we get here, player is in violation. + -- make sure we will not trigger again by setting future speedingsince to negative + taxiPolice.suspects[player] = timer.getTime() + 10000 -- 10000 seconds in the future + + local vioNum = taxiPolice.tickets[player] + if not vioNum then vioNum = 0 end + vioNum = vioNum + 1 + taxiPolice.tickets[player] = vioNum + + local pGrp = theUnit:getGroup() + local gID = pGrp:getID() + + if vioNum <= taxiPolice.maxTickets then + -- just post a warning + trigger.action.outTextForGroup(gID, player .. ", your taxi speed is reckless. Stop it. Violations registered against you: " .. vioNum, 30) + return + end + + -- we have reached retribution stage + taxiPolice.retributeAgainst(theUnit) +end + +--- +--- UPDATE +--- +function taxiPolice.update() -- every second + -- schedule next invocation + timer.scheduleFunction(taxiPolice.update, {}, timer.getTime() + 1/taxiPolice.ups) + + local allAirfields = dcsCommon.getAirbasesWhoseNameContains("*", 0) -- all fixed bases, no FARP nor ships. Pre-collect + + -- check all player units + local playerFactions = {1, 2} + for idx, aFaction in pairs(playerFactions) do + local allPlayers = coalition.getPlayers(aFaction) + for idy, aPlayer in pairs(allPlayers) do -- returns UNITS! + if Unit.isActive(aPlayer) and not aPlayer:inAir() then + taxiPolice.checkUnit(aPlayer, allAirfields) + end + end + end + +end + +-- +-- START +-- +function taxiPolice.processTaxiZones() + local taxiZones = cfxZones.zonesWithProperty("taxiPolice") + local allAirfields = dcsCommon.getAirbasesWhoseNameContains("*", 0) + + for idx, theZone in pairs(taxiZones) do + local isPoliced = cfxZones.getBoolFromZoneProperty(theZone, "taxiPolice", "true") + if not isPoliced then + local p = cfxZones.getPoint(theZone) + local base, dist = dcsCommon.getClosestAirbaseTo(p, nil, nil, allAirfields) + local name = base:getName() + taxiPolice.runways[name] = nil + if taxiPolice.verbose then + trigger.action.outText("txPol: base <" .. name .. "> taxiways not policed.", 30) + end + end + end +end + +function taxiPolice.readConfigZone() + local theZone = cfxZones.getZoneByName("taxiPoliceConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("taxiPoliceConfig") + if taxiPolice.verbose then + trigger.action.outText("+++txPol: no config zone!", 30) + end + end + taxiPolice.verbose = theZone.verbose + + taxiPolice.speedLimit = cfxZones.getNumberFromZoneProperty(theZone, "speedLimit", 14) -- 14 -- m/s. 14 m/s = 50 km/h, 10 m/s = 36 kmh + taxiPolice.triggerTime = cfxZones.getNumberFromZoneProperty(theZone, "triggerTime", 3) --3 -- seconds until we register a speeding violation + taxiPolice.rwyLeeway = cfxZones.getNumberFromZoneProperty(theZone, "leeway", 5) -- 5 -- meters on each side + taxiPolice.rwyExtend = cfxZones.getNumberFromZoneProperty(theZone, "extend", 500) --500 -- meters in front and at end + taxiPolice.airfieldMaxDist = cfxZones.getNumberFromZoneProperty(theZone, "radius", 3000) -- 3000 -- radius around airfield in which we operate + taxiPolice.maxTickets = cfxZones.getNumberFromZoneProperty(theZone, "maxTickets", 3) -- 3 + +end + +function taxiPolice.start() + -- lib check + if not dcsCommon.libCheck then + trigger.action.outText("cfx taxiPolice requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx taxiPolice", taxiPolice.requiredLibs) then + return false + end + + -- read config + taxiPolice.readConfigZone() + + -- build taxiway db + taxiPolice.buildRunways() + + -- read taxiPolice attributes + taxiPolice.processTaxiZones() + + -- start update + taxiPolice.update() + + -- say hi! + trigger.action.outText("cfx taxiPolice v" .. taxiPolice.version .. " started.", 30) + return true +end + +-- let's go! +if not taxiPolice.start() then + trigger.action.outText("cfx taxiPolice aborted: missing libraries", 30) + taxiPolice = nil +end + + + diff --git a/tutorial & demo missions/demo - recon mode - reloaded.miz b/tutorial & demo missions/demo - recon mode - reloaded.miz index 86af1c5..62d9bf3 100644 Binary files a/tutorial & demo missions/demo - recon mode - reloaded.miz and b/tutorial & demo missions/demo - recon mode - reloaded.miz differ