Version 1.4.1

Civ Air off-map locations
Guardian Angel updates
Recon Mode updates
This commit is contained in:
Christian Franz 2023-08-10 11:45:07 +02:00
parent 3117bfd80c
commit 7c6deec7a6
11 changed files with 704 additions and 206 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
cfxReconMode = {}
cfxReconMode.version = "2.1.4"
cfxReconMode.version = "2.2.0"
cfxReconMode.verbose = false -- set to true for debug info
cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered
@ -86,6 +86,10 @@ VERSION HISTORY
2.1.3 - added cfxReconMode.name to allow direct acces with test zone flag
2.1.4 - canDetect() also checks if unit has been activated
canDetect has strenghtened isExist() guard
2.2.0 - new marksLocked config attribute, defaults to false
- new marksFadeAfter config attribute to control mark time
- dmlZones OOP upgrade
cfxReconMode is a script that allows units to perform reconnaissance
missions and, after detecting units, marks them on the map with
@ -355,7 +359,7 @@ function cfxReconMode.placeMarkForUnit(location, theSide, theGroup)
theDesc,
location,
theSide,
false,
cfxReconMode.marksLocked, -- readOnly -- false,
nil)
return theID
end
@ -492,7 +496,7 @@ function cfxReconMode.processZoneMessage(inMsg, theZone, theGroup)
-- replace <lat> with lat of zone point and <lon> with lon of zone point
-- and <mgrs> with mgrs coords of zone point
local currPoint = cfxZones.getPoint(theZone)
local currPoint = theZone:getPoint()
if theGroup and theGroup:isExist() then
-- only use group's point when group exists and alive
local theUnit = dcsCommon.getFirstLivingUnit(theGroup)
@ -529,7 +533,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc)
end
-- put a mark on the map
if not silent and cfxReconMode.applyMarks then
if (not silent) and cfxReconMode.applyMarks then
local theID = cfxReconMode.placeMarkForUnit(theLoc, mySide, theGroup)
local gName = theGroup:getName()
local args = {mySide, theScout, theGroup, theID, gName}
@ -541,10 +545,9 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc)
end
-- say something
if not silent and cfxReconMode.announcer then
if (not silent) and cfxReconMode.announcer then
local msg = cfxReconMode.generateSALT(theScout, theGroup)
trigger.action.outTextForCoalition(mySide, msg, cfxReconMode.reportTime)
-- trigger.action.outTextForCoalition(mySide, theScout:getName() .. " reports new ground contact " .. theGroup:getName(), 30)
if cfxReconMode.verbose then
trigger.action.outText("+++rcn: announced for side " .. mySide, 30)
end
@ -554,7 +557,6 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc)
end
-- see if it was a prio target
--local inList, gName = cfxReconMode.isStringInList(theGroup:getName(), cfxReconMode.prioList)
if inList then
-- if cfxReconMode.announcer then
if cfxReconMode.verbose then
@ -565,7 +567,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc)
-- increase prio flag
if cfxReconMode.prioFlag then
cfxZones.pollFlag(cfxReconMode.prioFlag, cfxReconMode.method, cfxReconMode.theZone)
cfxReconMode.theZone:pollFlag(cfxReconMode.prioFlag, cfxReconMode.method )
end
-- see if we were passed additional info in zInfo
@ -583,7 +585,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc)
end
if zInfo.theFlag then
cfxZones.pollFlag(zInfo.theFlag, cfxReconMode.method, zInfo.theZone)
zInfo.theZone:pollFlag(zInfo.theFlag, cfxReconMode.method)
if cfxReconMode.verbose or zInfo.theZone.verbose then
trigger.action.outText("+++rcn: banging <" .. zInfo.theFlag .. "> for prio target zone <" .. zInfo.theZone.name .. ">",30)
end
@ -595,7 +597,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc)
-- increase normal flag
if cfxReconMode.detectFlag then
cfxZones.pollFlag(cfxReconMode.detectFlag, cfxReconMode.method, cfxReconMode.theZone)
cfxReconMode.theZone:pollFlag(cfxReconMode.detectFlag, cfxReconMode.method)
end
end
end
@ -655,7 +657,7 @@ function cfxReconMode.doDeActivate()
end
end
function cfxReconMode.updateQueues()
function cfxReconSMode.updateQueues()
-- schedule next call
timer.scheduleFunction(cfxReconMode.updateQueues, {}, timer.getTime() + 1/cfxReconMode.ups)
@ -681,7 +683,6 @@ function cfxReconMode.updateQueues()
-- the scouts array, move it to processed and then shrink
-- scouts table until it's empty. When empty, transfer all
-- back and start cycle anew
local theFocusScoutName = nil
local procCount = 0 -- no iterations done yet
for name, scout in pairs(cfxReconMode.scouts) do
@ -736,7 +737,6 @@ function cfxReconMode.autoRemove()
local toRemove = {}
-- scan all marked groups, and when they no longer exist, remove them
for idx, args in pairs (cfxReconMode.activeMarks) do
-- args = {mySide, theScout, theGroup, theID, gName}
local gName = args[5]
if not cfxReconMode.isGroupStillAlive(gName) then
-- remove mark, remove group from set
@ -766,7 +766,7 @@ function cfxReconMode.lateEvalPlayerUnit(theUnit)
for idx, theZone in pairs (cfxReconMode.scoutZones) do
local isScout = theZone.isScout
local dynamic = theZone.dynamic
local inZone = cfxZones.pointInZone(p, theZone)
local inZone = theZone:pointInZone(p)
if inZone then
if isScout then
cfxReconMode.addToAllowedScoutList(aGroup, dynamic)
@ -886,7 +886,6 @@ function cfxReconMode:onEvent(event)
end
cfxReconMode.addScout(theUnit)
end
-- trigger.action.outText("+++rcn-onEvent: " .. event.id .. " for <" .. theUnit:getName() .. ">", 30)
end
--
@ -984,68 +983,70 @@ function cfxReconMode.readConfigZone()
end
end
cfxReconMode.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
cfxReconMode.verbose = theZone.verbose --cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
cfxReconMode.autoRecon = cfxZones.getBoolFromZoneProperty(theZone, "autoRecon", true)
cfxReconMode.redScouts = cfxZones.getBoolFromZoneProperty(theZone, "redScouts", false)
cfxReconMode.blueScouts = cfxZones.getBoolFromZoneProperty(theZone, "blueScouts", true)
cfxReconMode.greyScouts = cfxZones.getBoolFromZoneProperty(theZone, "greyScouts", false)
cfxReconMode.playerOnlyRecon = cfxZones.getBoolFromZoneProperty(theZone, "playerOnlyRecon", false)
cfxReconMode.reportNumbers = cfxZones.getBoolFromZoneProperty(theZone, "reportNumbers", true)
cfxReconMode.reportTime = cfxZones.getNumberFromZoneProperty(theZone, "reportTime", 30)
cfxReconMode.autoRecon = theZone:getBoolFromZoneProperty("autoRecon", true)
cfxReconMode.redScouts = theZone:getBoolFromZoneProperty("redScouts", false)
cfxReconMode.blueScouts = theZone:getBoolFromZoneProperty( "blueScouts", true)
cfxReconMode.greyScouts = theZone:getBoolFromZoneProperty( "greyScouts", false)
cfxReconMode.playerOnlyRecon = theZone:getBoolFromZoneProperty("playerOnlyRecon", false)
cfxReconMode.reportNumbers = theZone:getBoolFromZoneProperty( "reportNumbers", true)
cfxReconMode.reportTime = theZone:getNumberFromZoneProperty( "reportTime", 30)
cfxReconMode.detectionMinRange = cfxZones.getNumberFromZoneProperty(theZone, "detectionMinRange", 3000)
cfxReconMode.detectionMaxRange = cfxZones.getNumberFromZoneProperty(theZone, "detectionMaxRange", 12000)
cfxReconMode.maxAlt = cfxZones.getNumberFromZoneProperty(theZone, "maxAlt", 9000)
cfxReconMode.detectionMinRange = theZone:getNumberFromZoneProperty("detectionMinRange", 3000)
cfxReconMode.detectionMaxRange = theZone:getNumberFromZoneProperty("detectionMaxRange", 12000)
cfxReconMode.maxAlt = theZone:getNumberFromZoneProperty("maxAlt", 9000)
if cfxZones.hasProperty(theZone, "prio+") then
cfxReconMode.prioFlag = cfxZones.getStringFromZoneProperty(theZone, "prio+", "none")
elseif cfxZones.hasProperty(theZone, "prio!") then
cfxReconMode.prioFlag = cfxZones.getStringFromZoneProperty(theZone, "prio!", "*<none>")
if theZone:hasProperty("prio+") then -- deprecated. remove next update
cfxReconMode.prioFlag = theZone:getStringFromZoneProperty("prio+", "none")
elseif theZone:hasProperty("prio!") then
cfxReconMode.prioFlag = theZone:getStringFromZoneProperty("prio!", "*<none>")
end
if cfxZones.hasProperty(theZone, "detect+") then
cfxReconMode.detectFlag = cfxZones.getStringFromZoneProperty(theZone, "detect+", "none")
elseif cfxZones.hasProperty(theZone, "detect!") then
cfxReconMode.detectFlag = cfxZones.getStringFromZoneProperty(theZone, "detect!", "*<none>")
if theZone:hasProperty("detect+") then -- deprecated
cfxReconMode.detectFlag = theZone:getStringFromZoneProperty("detect+", "none")
elseif theZone:hasProperty("detect!") then
cfxReconMode.detectFlag = theZone:getStringFromZoneProperty("detect!", "*<none>")
end
cfxReconMode.method = cfxZones.getStringFromZoneProperty(theZone, "method", "inc")
if cfxZones.hasProperty(theZone, "reconMethod") then
cfxReconMode.method = cfxZones.getStringFromZoneProperty(theZone, "reconMethod", "inc")
cfxReconMode.method = theZone:getStringFromZoneProperty("method", "inc")
if theZone:hasProperty("reconMethod") then
cfxReconMode.method = theZone:getStringFromZoneProperty("reconMethod", "inc")
end
cfxReconMode.applyMarks = cfxZones.getBoolFromZoneProperty(theZone, "applyMarks", true)
cfxReconMode.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true)
-- trigger.action.outText("recon: announcer is " .. dcsCommon.bool2Text(cfxReconMode.announcer), 30) -- announced
if cfxZones.hasProperty(theZone, "reconSound") then
cfxReconMode.reconSound = cfxZones.getStringFromZoneProperty(theZone, "reconSound", "<nosound>")
cfxReconMode.applyMarks = theZone:getBoolFromZoneProperty( "applyMarks", true)
cfxReconMode.marksFadeAfter = theZone:getNumberFromZoneProperty("marksFadeAfter", 30*60) -- 30 minutes default
cfxReconMode.marksLocked = theZone:getBoolFromZoneProperty("marksLocked", false) -- if true, players cannot remove the marks
cfxReconMode.announcer = theZone:getBoolFromZoneProperty( "announcer", true)
if theZone:hasProperty("reconSound") then
cfxReconMode.reconSound = theZone:getStringFromZoneProperty("reconSound", "<nosound>")
end
cfxReconMode.removeWhenDestroyed = cfxZones.getBoolFromZoneProperty(theZone, "autoRemove", true)
cfxReconMode.removeWhenDestroyed = theZone:getBoolFromZoneProperty("autoRemove", true)
cfxReconMode.mgrs = cfxZones.getBoolFromZoneProperty(theZone, "mgrs", false)
cfxReconMode.mgrs = theZone:getBoolFromZoneProperty("mgrs", false)
cfxReconMode.active = cfxZones.getBoolFromZoneProperty(theZone, "active", true)
if cfxZones.hasProperty(theZone, "activate?") then
cfxReconMode.activate = cfxZones.getStringFromZoneProperty(theZone, "activate?", "*<none>")
cfxReconMode.lastActivate = cfxZones.getFlagValue(cfxReconMode.activate, theZone)
elseif cfxZones.hasProperty(theZone, "on?") then
cfxReconMode.activate = cfxZones.getStringFromZoneProperty(theZone, "on?", "*<none>")
cfxReconMode.lastActivate = cfxZones.getFlagValue(cfxReconMode.activate, theZone)
cfxReconMode.active = theZone:getBoolFromZoneProperty("active", true)
if theZone:hasProperty("activate?") then
cfxReconMode.activate = theZone:getStringFromZoneProperty("activate?", "*<none>")
cfxReconMode.lastActivate = theZone:getFlagValue(cfxReconMode.activate)
elseif theZone:hasProperty("on?") then
cfxReconMode.activate = theZone:getStringFromZoneProperty("on?", "*<none>")
cfxReconMode.lastActivate = theZone:getFlagValue(cfxReconMode.activate)
end
if cfxZones.hasProperty(theZone, "deactivate?") then
cfxReconMode.deactivate = cfxZones.getStringFromZoneProperty(theZone, "deactivate?", "*<none>")
cfxReconMode.lastDeActivate = cfxZones.getFlagValue(cfxReconMode.deactivate, theZone)
elseif cfxZones.hasProperty(theZone, "off?") then
cfxReconMode.deactivate = cfxZones.getStringFromZoneProperty(theZone, "off?", "*<none>")
cfxReconMode.lastDeActivate = cfxZones.getFlagValue(cfxReconMode.deactivate, theZone)
if theZone:hasProperty("deactivate?") then
cfxReconMode.deactivate = theZone:getStringFromZoneProperty("deactivate?", "*<none>")
cfxReconMode.lastDeActivate = theZone:getFlagValue(cfxReconMode.deactivate)
elseif theZone:hasProperty("off?") then
cfxReconMode.deactivate = theZone:getStringFromZoneProperty("off?", "*<none>")
cfxReconMode.lastDeActivate = theZone:getFlagValue(cfxReconMode.deactivate)
end
cfxReconMode.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperial", false)
if cfxZones.hasProperty(theZone, "imperialUnits") then
cfxReconMode.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperialUnits", false)
cfxReconMode.imperialUnits = theZone:getBoolFromZoneProperty("imperial", false)
if theZone:hasProperty("imperialUnits") then
cfxReconMode.imperialUnits = theZone:getBoolFromZoneProperty( "imperialUnits", false)
end
cfxReconMode.theZone = theZone -- save this zone
@ -1057,24 +1058,24 @@ end
function cfxReconMode.processReconZone(theZone)
local theList = cfxZones.getStringFromZoneProperty(theZone, "recon", "prio")
local theList = theZone:getStringFromZoneProperty("recon", "prio")
theList = string.upper(theList)
local isBlack = dcsCommon.stringStartsWith(theList, "BLACK")
local zInfo = {}
zInfo.theZone = theZone
zInfo.isBlack = isBlack
zInfo.silent = cfxZones.getBoolFromZoneProperty(theZone, "silent", false)
zInfo.silent = theZone:getBoolFromZoneProperty("silent", false)
if cfxZones.hasProperty(theZone, "spotted!") then
zInfo.theFlag = cfxZones.getStringFromZoneProperty(theZone, "spotted!", "*<none>")
if theZone:hasProperty("spotted!") then
zInfo.theFlag = theZone:getStringFromZoneProperty("spotted!", "*<none>")
end
if cfxZones.hasProperty(theZone, "prioMessage") then
zInfo.prioMessage = cfxZones.getStringFromZoneProperty(theZone, "prioMessage", "<none>")
if theZone:hasProperty("prioMessage") then
zInfo.prioMessage = theZone:getStringFromZoneProperty("prioMessage", "<none>")
end
local dynamic = cfxZones.getBoolFromZoneProperty(theZone, "dynamic", false)
local dynamic = theZone:getBoolFromZoneProperty("dynamic", false)
zInfo.dynamic = dynamic
local categ = 2 -- ground troops only
local allGroups = cfxZones.allGroupsInZone(theZone, categ)
@ -1100,15 +1101,15 @@ function cfxReconMode.processReconZone(theZone)
end
function cfxReconMode.processScoutZone(theZone)
local isScout = cfxZones.getBoolFromZoneProperty(theZone, "scout", true)
local dynamic = cfxZones.getBoolFromZoneProperty(theZone, "dynamic")
local isScout = theZone:getBoolFromZoneProperty("scout", true)
local dynamic = theZone:getBoolFromZoneProperty("dynamic")
theZone.dynamic = dynamic
theZone.isScout = isScout
local categ = 0 -- aircraft
local allFixed = cfxZones.allGroupsInZone(theZone, categ)
local allFixed = theZone:allGroupsInZone(categ)
local categ = 1 -- helos
local allRotor = cfxZones.allGroupsInZone(theZone, categ)
local allRotor = theZone:allGroupsInZone(categ)
local allGroups = dcsCommon.combineTables(allFixed, allRotor)
for idx, aGroup in pairs(allGroups) do
if isScout then

View File

@ -149,6 +149,7 @@ cfxZones.version = "4.0.0"
- getNumberFromZoneProperty() enforces number return even on default
- immediate method switched to preceeding '#', to resolve conflict witzh
negative numbers, backwards compatibility with old (dysfunctional) method
- 4.0.1 - dmlZone:getName()
--]]--
--
@ -3258,6 +3259,10 @@ function dmlZone:getPoint(getHeight)
return thePos
end
function dmlZone:getName() -- no cfxZones.bridge!
return self.name
end
function cfxZones.linkUnitToZone(theUnit, theZone, dx, dy) -- note: dy is really Z, don't get confused!!!!
theZone.linkedUnit = theUnit
if not dx then dx = 0 end

View File

@ -1,5 +1,5 @@
civAir = {}
civAir.version = "1.5.2"
civAir.version = "2.0.0"
--[[--
1.0.0 initial version
1.1.0 exclude list for airfields
@ -23,7 +23,15 @@ civAir.version = "1.5.2"
exclude list and include list
1.5.1 added depart only and arrive only options for airfields
1.5.2 fixed bugs inb verbosity
2.0.0 dmlZones
inbound zones
outbound zones
on start location is randomizes 30-70% of the way
guarded 'no longer exist' warning for verbosity
changed unit naming from -civA to -GA
strenghtened guard on testing against free slots for other units
flights are now of random neutral countries
maxFlights synonym for maxTraffic
--]]--
@ -48,22 +56,22 @@ civAir.trafficCenters = {}
-- If the attribute's value is anything
-- but "exclude", the closest airfield to the zone
-- is added to trafficCenters
-- if you leave this list empty, and do not add airfields
-- by zones, the list is automatically populated with all
-- airfields in the map
-- if name starts with "***" then it is not an airfield, but zone
civAir.excludeAirfields = {}
-- list all airfields that must NOT be included in
-- civilian activities. Will be used for neither landing
-- nor departure. overrides any airfield that was included
-- in trafficCenters. Here, Senaki is off limits for
-- civilian air traffic
-- in trafficCenters.
-- can be populated by zone on the map that have the
-- 'civAir' attribute with value "exclude"
civAir.departOnly = {} -- use only to start from
civAir.landingOnly = {} -- use only to land at
civAir.inoutZones = {} -- off-map connector zones
civAir.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
@ -72,57 +80,73 @@ civAir.requiredLibs = {
civAir.activePlanes = {}
civAir.idlePlanes = {}
civAir.outboundFlights = {} -- only flights that are enroute to an outbound zone
function civAir.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("civAirConfig")
if not theZone then
trigger.action.outText("***civA: NO config zone!", 30)
return
theZone = cfxZones.createSimpleZone("civAirConfig")
end
trigger.action.outText("civA: found config zone!", 30)
-- ok, for each property, load it if it exists
if cfxZones.hasProperty(theZone, "aircraftTypes") then
local theTypes = cfxZones.getStringFromZoneProperty(theZone, "aircraftTypes", "Yak-40")
if theZone:hasProperty("aircraftTypes") then
local theTypes = theZone:getStringFromZoneProperty( "aircraftTypes", civAir.aircraftTypes) -- "Yak-40")
local typeArray = dcsCommon.splitString(theTypes, ",")
typeArray = dcsCommon.trimArray(typeArray)
civAir.aircraftTypes = typeArray
end
if cfxZones.hasProperty(theZone, "ups") then
civAir.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 0.05)
if theZone:hasProperty("ups") then
civAir.ups = theZone:getNumberFromZoneProperty("ups", 0.05)
if civAir.ups < .0001 then civAir.ups = 0.05 end
end
if cfxZones.hasProperty(theZone, "maxTraffic") then
civAir.maxTraffic = cfxZones.getNumberFromZoneProperty(theZone, "maxTraffic", 10)
if theZone:hasProperty("maxTraffic") then
civAir.maxTraffic = theZone:getNumberFromZoneProperty( "maxTraffic", 10)
elseif theZone:hasProperty("maxFlights") then
civAir.maxTraffic = theZone:getNumberFromZoneProperty( "maxFlights", 10)
end
if cfxZones.hasProperty(theZone, "maxIdle") then
civAir.maxIdle = cfxZones.getNumberFromZoneProperty(theZone, "maxIdle", 8 * 60)
if theZone:hasProperty("maxIdle") then
civAir.maxIdle = theZone:getNumberFromZoneProperty("maxIdle", 8 * 60)
end
if cfxZones.hasProperty(theZone, "initialAirSpawns") then
civAir.initialAirSpawns = cfxZones.getBoolFromZoneProperty(theZone, "initialAirSpawns", true)
if theZone:hasProperty("initialAirSpawns") then
civAir.initialAirSpawns = theZone:getBoolFromZoneProperty( "initialAirSpawns", true)
end
civAir.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
civAir.verbose = theZone.verbose -- cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
end
function civAir.processZone(theZone)
local value = cfxZones.getStringFromZoneProperty(theZone, "civAir", "")
local value = theZone:getStringFromZoneProperty("civAir", "")
local af = dcsCommon.getClosestAirbaseTo(theZone.point, 0) -- 0 = only airfields, not farp or ships
local inoutName = "***" .. theZone:getName()
if af then
local afName = af:getName()
value = value:lower()
if value == "exclude" then
if value == "exclude" or value == "closed" then
table.insert(civAir.excludeAirfields, afName)
elseif dcsCommon.stringStartsWith(value, "depart") or dcsCommon.stringStartsWith(value, "start") then
elseif dcsCommon.stringStartsWith(value, "depart") or dcsCommon.stringStartsWith(value, "start") or dcsCommon.stringStartsWith(value, "take") then
table.insert(civAir.departOnly, afName)
elseif dcsCommon.stringStartsWith(value, "land") or dcsCommon.stringStartsWith(value, "arriv") then
table.insert(civAir.landingOnly, afName)
elseif dcsCommon.stringStartsWith(value, "inb") then
table.insert(civAir.departOnly, inoutName) -- start in inbound zone
civAir.inoutZones[inoutName] = theZone
-- theZone.inbound = true
elseif dcsCommon.stringStartsWith(value, "outb") then
table.insert(civAir.landingOnly, inoutName)
civAir.inoutZones[inoutName] = theZone
-- theZone.outbound = true
elseif dcsCommon.stringStartsWith(value, "in/out") then
table.insert(civAir.trafficCenters, inoutName)
civAir.inoutZones[inoutName] = theZone
-- theZone.inbound = true
-- theZone.outbound = true
else
table.insert(civAir.trafficCenters, afName) -- note that adding the same twice makes it more likely to be picked
end
@ -142,10 +166,11 @@ function civAir.removePlaneGroupByName(aName)
return
end
if civAir.activePlanes[aName] then
--trigger.action.outText("civA: REMOVING " .. aName .. " ***", 30)
civAir.activePlanes[aName] = nil
else
trigger.action.outText("civA: warning - ".. aName .." remove req but not found", 30)
if civAir.verbose then
trigger.action.outText("civA: warning - ".. aName .." remove req but not found", 30)
end
end
end
@ -207,10 +232,25 @@ function civAir.getTwoAirbases()
tries = tries + 1 -- only try 10 times
until fAB ~= sAB or tries > 10
fAB = dcsCommon.getFirstAirbaseWhoseNameContains(fAB, 0)
sAB = dcsCommon.getFirstAirbaseWhoseNameContains(sAB, 0)
local civA = {}
if not (dcsCommon.stringStartsWith(fAB, '***')) then
civA.AB = dcsCommon.getFirstAirbaseWhoseNameContains(fAB, 0)
civA.name = civA.AB:getName()
else
civA.zone = civAir.inoutZones[fAB]
civA.name = civA.zone:getName()
end
local civB = {}
if not (dcsCommon.stringStartsWith(sAB, '***')) then
civB.AB = dcsCommon.getFirstAirbaseWhoseNameContains(sAB, 0)
civB.name = civB.AB:getName()
else
civB.zone = civAir.inoutZones[sAB]
civB.name = civB.zone:getName()
end
return fAB, sAB
return civA, civB -- fAB, sAB
end
function civAir.parkingIsFree(fromWP)
@ -222,9 +262,9 @@ function civAir.parkingIsFree(fromWP)
loc.z = fromWP.z
for name, aPlaneGroup in pairs(civAir.activePlanes) do
if aPlaneGroup:isExist() then
if Group.isExist(aPlaneGroup) then
local aPlane = aPlaneGroup:getUnit(1)
if aPlane:isExist() then
if aPlane and Unit.isExist(aPlane) then
pos = aPlane:getPoint()
local delta = dcsCommon.dist(loc, pos)
if delta < 21 then
@ -242,21 +282,35 @@ end
civAir.airStartSeparation = 0
function civAir.createFlight(name, theTypeString, fromAirfield, toAirfield, inAirStart)
if not fromAirfield then
trigger.action.outText("civA: NIL fromAirfield", 30)
trigger.action.outText("civA: NIL source", 30)
return nil
end
if not toAirfield then
trigger.action.outText("civA: NIL toAirfield", 30)
trigger.action.outText("civA: NIL destination", 30)
return nil
end
local randomizeLoc = inAirStart
local theGroup = dcsCommon.createEmptyAircraftGroupData (name)
local theAUnit = dcsCommon.createAircraftUnitData(name .. "-civA", theTypeString, false)
local theAUnit = dcsCommon.createAircraftUnitData(name .. "-GA", theTypeString, false)
theAUnit.payload.fuel = 100000
dcsCommon.addUnitToGroupData(theAUnit, theGroup)
local fromWP = dcsCommon.createTakeOffFromParkingRoutePointData(fromAirfield)
local fromWP
if fromAirfield.AB then
fromWP = dcsCommon.createTakeOffFromParkingRoutePointData(fromAirfield.AB)
else
-- we start in air from inside inbound zone
local p = fromAirfield.zone:createRandomPointInZone()
local alt = fromAirfield.zone:getNumberFromZoneProperty("alt", 8000)
fromWP = dcsCommon.createSimpleRoutePointData(p, alt)
theAUnit.alt = fromWP.alt
theAUnit.speed = fromWP.speed
inAirStart = false -- it already is, no separation shenigans
end
if not fromWP then
trigger.action.outText("civA: fromWP create failed", 30)
return nil
@ -266,38 +320,81 @@ function civAir.createFlight(name, theTypeString, fromAirfield, toAirfield, inAi
fromWP.alt = fromWP.alt + 3000 + civAir.airStartSeparation -- 9000 ft overhead + separation
fromWP.action = "Turning Point"
fromWP.type = "Turning Point"
fromWP.speed = 150;
fromWP.airdromeId = nil
theAUnit.alt = fromWP.alt
theAUnit.speed = fromWP.speed
end
-- sometimes, when landing kicks in too early, the plane lands
-- at the wrong airfield. AI sucks.
-- so we force overflight of target airfield
local overheadWP = dcsCommon.createOverheadAirdromeRoutPintData(toAirfield)
local toWP = dcsCommon.createLandAtAerodromeRoutePointData(toAirfield)
if not toWP then
trigger.action.outText("civA: toWP create failed", 30)
return nil
end
if not civAir.parkingIsFree(fromWP) then
trigger.action.outText("civA: failed free parking check for flight " .. name, 30)
return nil
-- now look at destination: airfield or zone?
local zoneApproach = toAirfield.zone
local toWP
local overheadWP
if zoneApproach then
-- we fly this plane to a zone, and then disappear it
local p = zoneApproach:getPoint()
local alt = zoneApproach:getNumberFromZoneProperty("alt", 8000)
toWP = dcsCommon.createSimpleRoutePointData(p, alt)
else
-- sometimes, when landing kicks in too early, the plane lands
-- at the wrong airfield. AI sucks.
-- so we force overflight of target airfield
overheadWP = dcsCommon.createOverheadAirdromeRoutPintData(toAirfield.AB)
toWP = dcsCommon.createLandAtAerodromeRoutePointData(toAirfield.AB)
if not toWP then
trigger.action.outText("civA: toWP create failed", 30)
return nil
end
if not civAir.parkingIsFree(fromWP) then
trigger.action.outText("civA: failed free parking check for flight " .. name, 30)
return nil
end
end
if randomizeLoc then
-- make first wp to somewhere 30-70 towards toWP
local percent = (math.random(40) + 30) / 100
local mx = dcsCommon.lerp(fromWP.x, toWP.x, percent)
local my = dcsCommon.lerp(fromWP.y, toWP.y, percent)
fromWP.x = mx
fromWP.y = my
fromWP.speed = 150
fromWP.alt = 8000
theAUnit.alt = fromWP.alt
theAUnit.speed = fromWP.speed
end
if (not fromAirfield.AB) or randomizedLoc or inAirStart then
-- set current heading correct towards toWP
local hdg = dcsCommon.bearingFromAtoBusingXY(fromWP, toWP)
theAUnit.heading = hdg
theAUnit.psi = -hdg
end
dcsCommon.moveGroupDataTo(theGroup,
fromWP.x,
fromWP.y)
dcsCommon.addRoutePointForGroupData(theGroup, fromWP)
dcsCommon.addRoutePointForGroupData(theGroup, overheadWP)
if not zoneApproach then
dcsCommon.addRoutePointForGroupData(theGroup, overheadWP)
end
dcsCommon.addRoutePointForGroupData(theGroup, toWP)
-- spawn
local groupCat = Group.Category.AIRPLANE
local theSpawnedGroup = coalition.addGroup(82, groupCat, theGroup) -- 82 is UN peacekeepers
local allNeutral = dcsCommon.getCountriesForCoalition(0)
local aRandomNeutral = dcsCommon.pickRandom(allNeutral)
if not aRandomNeutral then
trigger.action.outText("+++civA: WARNING: no neutral countries exist, flight is not neutral.", 30)
end
local theSpawnedGroup = coalition.addGroup(aRandomNeutral, groupCat, theGroup) -- 82 is UN peacekeepers
if zoneApproach then
-- track this flight to target zone
civAir.outboundFlights[name] = zoneApproach
end
return theSpawnedGroup
end
@ -312,7 +409,9 @@ function civAir.createNewFlight(inAirStart)
return
end
local name = fAB:getName() .. "-" .. sAB:getName().. "/" .. civAir.flightCount
-- fAB and sAB are tables that have either .base or AB set
local name = fAB.name .. "-" .. sAB.name.. "/" .. civAir.flightCount
local TypeString = dcsCommon.pickRandom(civAir.aircraftTypes)
local theFlight = civAir.createFlight(name, TypeString, fAB, sAB, inAirStart)
@ -325,7 +424,7 @@ function civAir.createNewFlight(inAirStart)
civAir.addPlane(theFlight) -- track it
if civAir.verbose then
trigger.action.outText("civA: created flight from <" .. fAB:getName() .. "> to <" .. sAB:getName() .. ">", 30)
trigger.action.outText("civA: created flight from <" .. fAB.name .. "> to <" .. sAB.name .. ">", 30)
end
end
@ -340,9 +439,40 @@ function civAir.airStartPopulation()
end
--
-- U P D A T E L O O P
-- U P D A T E L O O P S
--
function civAir.trackOutbound()
timer.scheduleFunction(civAir.trackOutbound, {}, timer.getTime() + 10)
-- iterate all flights that are outbound
local filtered = {}
for gName, theZone in pairs(civAir.outboundFlights) do
local theGroup = Group.getByName(gName)
if theGroup then
local theUnit = theGroup:getUnit(1)
if theUnit and Unit.isExist(theUnit) then
local p = theUnit:getPoint()
local t = theZone:getPoint()
local d = dcsCommon.distFlat(p, t)
if d > 3000 then -- works unless plane faster than 300m/s = 1080 km/h
-- keep watching
filtered[gName] = theZone
else
-- we can disappear the group
if civAir.verbose then
trigger.action.outText("+++civA: flight <" .. gName .. "> has reached map outbound zone <" .. theZone:getName() .. "> and is removed", 30)
end
Group.destroy(theGroup)
end
else
trigger.action.outText("+++civ: lost unit in group <" .. gName .. "> heading for <" .. theZone:getName() .. ">", 30)
end
end
end
civAir.outboundFlights = filtered
end
function civAir.update()
-- reschedule me in the future. ups = updates per second.
timer.scheduleFunction(civAir.update, {}, timer.getTime() + 1/civAir.ups)
@ -359,12 +489,15 @@ function civAir.update()
for idx, name in pairs(removeMe) do
civAir.activePlanes[name] = nil
trigger.action.outText("civA: warning - removed " .. name .. " from active roster, no longer exists", 30)
if civAir.verbose then
trigger.action.outText("civA: removed " .. name .. " from active roster, no longer exists", 30)
end
end
-- now, run through all existing flights and update their
-- idle times. also count how many planes there are
-- so we can respawn if we are below max
local planeNum = 0
local overduePlanes = {}
local now = timer.getTime()
@ -372,18 +505,17 @@ function civAir.update()
local speed = 0
if aPlaneGroup:isExist() then
local aPlane = aPlaneGroup:getUnit(1)
if aPlane and aPlane:isExist() and aPlane:getLife() >= 1 then
if aPlane and Unit.isExist(aPlane) and aPlane:getLife() >= 1 then
planeNum = planeNum + 1
local vel = aPlane:getVelocity()
speed = dcsCommon.mag(vel.x, vel.y, vel.z)
else
-- force removal of group
-- force removal of group, plane no longer exists
civAir.idlePlanes[name] = -1000
speed = 0
end
else
-- force removal
-- force removal, group no longer exists
civAir.idlePlanes[name] = -1000
speed = 0
end
@ -398,10 +530,9 @@ function civAir.update()
table.insert(overduePlanes, name)
end
else
-- zero out idle plane
-- zero out idle plane, it's moving fast enough
civAir.idlePlanes[name] = nil
end
--]]--
end
-- see if we have less than max flights running
@ -414,9 +545,12 @@ function civAir.update()
for idx, aName in pairs(overduePlanes) do
local aFlight = civAir.getPlane(aName) -- returns a group
civAir.removePlaneGroupByName(aName) -- remove from roster
if aFlight and aFlight:isExist() then
if aFlight and Unit.isExist(aFlight) then
-- destroy can only work if group isexist!
Group.destroy(aFlight) -- remember: flights are groups!
if civAir.verbose then
trigger.action.outText("+++civA: removed flight <" .. aName .. "> for overtime.", 30)
end
end
end
end
@ -504,7 +638,9 @@ function civAir.start()
-- start the update loop
civAir.update()
-- start outbound tracking
civAir.trackOutbound()
-- say hi!
trigger.action.outText("cf/x civAir v" .. civAir.version .. " started.", 30)
return true
@ -518,10 +654,11 @@ end
--[[--
Additional ideas
- border zones: ac can airstart in there and disappear in there
- callbacks for civ spawn / despawn
- add civkill callback / redCivKill blueCivKill flag bangers
- Helicopter support
- departure only, destination only
- add slot checking to see if other planes block it even though DCS claims the slot is free
- allow list of countries to choose civ air from
- ability to force a flight from a source? How do we make a destination? currently not a good idea
--]]--

View File

@ -1,5 +1,5 @@
dcsCommon = {}
dcsCommon.version = "2.9.0"
dcsCommon.version = "2.9.2"
--[[-- VERSION HISTORY
2.2.6 - compassPositionOfARelativeToB
- clockPositionOfARelativeToB
@ -166,7 +166,12 @@ dcsCommon.version = "2.9.0"
2.9.0 - createPoint() moved from cfxZones
- copyPoint() moved from cfxZones
- numberArrayFromString() moved from cfxZones
2.9.1 - new createSimpleRoutePointData()
- createOverheadAirdromeRoutPintData corrected and legacy support added
- new bearingFromAtoBusingXY()
- corrected verbosity for bearingFromAtoB
- new getCountriesForCoalition()
2.9.2 - updated task2text
--]]--
@ -182,6 +187,7 @@ dcsCommon.version = "2.9.0"
dcsCommon.cbID = 0 -- callback id for simple callback scheduling
dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P"} -- Ka-50, Apache and Gazelle can't carry troops
dcsCommon.coalitionSides = {0, 1, 2}
dcsCommon.maxCountry = 86 -- number of countries defined in total
-- lookup tables
dcsCommon.groupID2Name = {}
@ -747,23 +753,23 @@ dcsCommon.version = "2.9.0"
return 0
end
if not B then
trigger.action.outText("WARNING: no 'A' in bearingFromAtoB", 30)
trigger.action.outText("WARNING: no 'B' in bearingFromAtoB", 30)
return 0
end
if not A.x then
trigger.action.outText("WARNING: no 'A.x' (type A =<" .. type(A) .. ">)in bearingFromAtoB", 30)
return 0
end
if not A.y then
trigger.action.outText("WARNING: no 'A.x' (type A =<" .. type(A) .. ">)in bearingFromAtoB", 30)
if not A.z then
trigger.action.outText("WARNING: no 'A.z' (type A =<" .. type(A) .. ">)in bearingFromAtoB", 30)
return 0
end
if not B.x then
trigger.action.outText("WARNING: no 'B.x' (type B =<" .. type(B) .. ">)in bearingFromAtoB", 30)
return 0
end
if not B.y then
trigger.action.outText("WARNING: no 'B.y' (type B =<" .. type(B) .. ">)in bearingFromAtoB", 30)
if not B.z then
trigger.action.outText("WARNING: no 'B.z' (type B =<" .. type(B) .. ">)in bearingFromAtoB", 30)
return 0
end
@ -773,6 +779,38 @@ dcsCommon.version = "2.9.0"
return bearing
end
function dcsCommon.bearingFromAtoBusingXY(A, B) -- coords in x, y
if not A then
trigger.action.outText("WARNING: no 'A' in bearingFromAtoBXY", 30)
return 0
end
if not B then
trigger.action.outText("WARNING: no 'B' in bearingFromAtoBXY", 30)
return 0
end
if not A.x then
trigger.action.outText("WARNING: no 'A.x' (type A =<" .. type(A) .. ">)in bearingFromAtoBXY", 30)
return 0
end
if not A.y then
trigger.action.outText("WARNING: no 'A.y' (type A =<" .. type(A) .. ">)in bearingFromAtoBXY", 30)
return 0
end
if not B.x then
trigger.action.outText("WARNING: no 'B.x' (type B =<" .. type(B) .. ">)in bearingFromAtoBXY", 30)
return 0
end
if not B.y then
trigger.action.outText("WARNING: no 'B.y' (type B =<" .. type(B) .. ">)in bearingFromAtoBXY", 30)
return 0
end
local dx = B.x - A.x
local dz = B.y - A.y
local bearing = math.atan2(dz, dx) -- in radiants
return bearing
end
function dcsCommon.bearingInDegreesFromAtoB(A, B)
local bearing = dcsCommon.bearingFromAtoB(A, B) -- in rads
bearing = math.floor(bearing / math.pi * 180)
@ -1116,7 +1154,7 @@ dcsCommon.version = "2.9.0"
-- coalition's countries
-- we start with id=0 (Russia), go to id=85 (Slovenia), but skip id = 14
local i = 0
while i < 86 do
while i < dcsCommon.maxCountry do -- 86 do
if i ~= 14 then
if (coalition.getCountryCoalition(i) == aCoalition) then return i end
end
@ -1125,6 +1163,22 @@ dcsCommon.version = "2.9.0"
return nil
end
function dcsCommon.getCountriesForCoalition(aCoalition)
if not aCoalition then aCoalition = 0 end
local allCty = {}
local i = 0
while i < dcsCommon.maxCountry do
if i ~= 14 then -- there is no county 14
if (coalition.getCountryCoalition(i) == aCoalition) then
table.insert(allCty, i)
end
end
i = i + 1
end
return allCty
end
--
--
-- C A L L B A C K H A N D L E R
@ -1394,7 +1448,7 @@ dcsCommon.version = "2.9.0"
return rp
end
function dcsCommon.createOverheadAirdromeRoutPintData(aerodrome)
function dcsCommon.createOverheadAirdromeRoutePointData(aerodrome)
if not aerodrome then return nil end
local rp = {}
local p = aerodrome:getPoint()
@ -1408,6 +1462,9 @@ dcsCommon.version = "2.9.0"
rp.alt_type = "BARO"
return rp
end
function dcsCommon.createOverheadAirdromeRoutPintData(aerodrome) -- backwards-compat to typo
return dcsCommon.createOverheadAirdromeRoutePointData(aerodrome)
end
function dcsCommon.createLandAtAerodromeRoutePointData(aerodrome)
@ -1418,7 +1475,7 @@ dcsCommon.version = "2.9.0"
rp.airdromeId = aerodrome:getID()
rp.x = p.x
rp.y = p.z
rp.alt = p.y
rp.alt = land.getHeight({x=p.x, y=p.z}) --p.y
rp.action = "Landing"
rp.type = "Land"
@ -1427,6 +1484,19 @@ dcsCommon.version = "2.9.0"
return rp
end
function dcsCommon.createSimpleRoutePointData(p, alt)
if not alt then alt = 8000 end -- 24'000 feet
local rp = {}
rp.x = p.x
rp.y = p.z
rp.alt = alt
rp.action = "Turning Point"
rp.type = "Turning Point"
rp.speed = 133; -- in m/s? If so, that's 360 km/h
rp.alt_type = "BARO"
return rp
end
function dcsCommon.createRPFormationData(findex) -- must be added as "task" to an RP. use 4 for Echelon right
local task = {}
@ -2481,8 +2551,8 @@ end
if not inrecursion then
-- output a marker to find in the log / screen
trigger.action.outText("=== dcsCommon vardump END", 30)
env.info("=== dcsCommon vardump END")
trigger.action.outText("=== dcsCommon vardump end", 30)
env.info("=== dcsCommon vardump end")
end
end
@ -2520,8 +2590,8 @@ end
if not inrecursion then
-- output a marker to find in the log / screen
trigger.action.outText("=== dcsCommon vardump END", 30)
--env.info("=== dcsCommon vardump END")
trigger.action.outText("=== dcsCommon vardump end", 30)
--env.info("=== dcsCommon vardump end")
end
end
@ -2541,15 +2611,19 @@ end
if id == 0 then return "invalid" end
-- translate the event id to text
local events = {"shot", "hit", "takeoff", "land",
"crash", "eject", "refuel", "dead",
"crash", "eject", "refuel", "dead", -- 8
"pilot dead", "base captured", "mission start", "mission end", -- 12
"took control", "refuel stop", "birth", "human failure",
"det. failure", "engine start", "engine stop", "player enter unit",
"player leave unit", "player comment", "start shoot", "end shoot",
"mark add", "mark changed", "makr removed", "kill",
"score", "unit lost", "land after eject", "Paratrooper land",
"chair discard after eject", "weapon add", "trigger zone", "landing quality mark",
"BDA", "max"}
"took control", "refuel stop", "birth", "human failure", -- 16
"det. failure", "engine start", "engine stop", "player enter unit", -- 20
"player leave unit", "player comment", "start shoot", "end shoot", -- 24
"mark add", "mark changed", "mark removed", "kill", -- 28
"score", "unit lost", "land after eject", "Paratrooper land", -- 32
"chair discard after eject", "weapon add", "trigger zone", "landing quality mark", -- 36
"BDA", "AI Abort Mission", "DayNight", "Flight Time", -- 40
"Pilot Suicide", "player cap airfield", "emergency landing", "unit create task", -- 44
"unit delete task", "Simulation start", "weapon rearm", "weapon drop", -- 48
"unit task timeout", "unit task stage",
"max"}
if id > #events then return "Unknown (ID=" .. id .. ")" end
return events[id]
end

253
modules/duel.lua Normal file
View File

@ -0,0 +1,253 @@
duel = {}
duel.version = "1.0.0"
duel.verbose = false
duel.requiredLibs = {
"dcsCommon",
"cfxZones",
"cfxMX",
}
--[[--
Version History
1.0.0 - Initial Version
--]]--
--[[--
ATTENTION!
- REQUIRES that SSB is running on the host
- REQUIRTES that SSB is confgured that '0' (zero) means slot is enabled (this is SSB default)
- This script must run at MISSION START and will enable SSB
--]]--
duel.duelZones = {}
duel.activeDuelists = {}
duel.allDuelists = {}
--
-- reading attributes
--
function duel.createDuelZone(theZone)
theZone.duelists = {} -- all player units in this zone
-- iterate all players and find any unit that is placed in this zone
for unitName, unitData in pairs(cfxMX.playerUnitByName) do
local p = {}
p.x = unitData.x
p.z = unitData.y -- !!
p.y = 0
if theZone:pointInZone(p) then
-- this is a player aircraft in this zone
local duelist = {}
duelist.data = unitData
duelist.name = unitName
duelist.type = unitData.type
local groupData = cfxMX.playerUnit2Group[unitName]
duelist.groupName = groupData.name
duelist.coa = cfxMX.groupCoalitionByName[duelist.groupName]
if duel.verbose then
trigger.action.outText("Detected player unit <" .. duelist.name .. ">, type <" .. duelist.type .. "> of group <" .. duelist.groupName .. "> of coa <" .. duelist.coa .. "> in zone <" .. theZone.name .. "> as duelist", 30)
end
duelist.active = false
duelist.arena = theZone.name
duelist.zone = theZone
-- enter into global table
-- player can only be in at maximum one duelist zones
if duel.allDuelists[unitName] then
trigger.action.outText("+++WARNING: overlapping duelists! Overwriting previous data", 30)
end
duel.allDuelists[unitName] = duelist
theZone.duelists[unitName] = duelist
end
end
theZone.state = "waiting" -- FSM, init to waiting state
end
--
-- Event processing
--
function duel.closeSlotsForZoneAndCoaExceptGroupNamed(theZone, coa, groupName)
-- iterate this zone's duelist groups and tell SSB to close them now
local allDuelists = theZone.duelists
for unitName, theDuelist in pairs(allDuelists) do
local dgName = theDuelist.groupName
if (theDuelist.coa == coa) and (dgName ~= groupName) then
if duel.verbose then
trigger.action.outText("+++duel: closing SSB slot for group <" .. dgName .. ">, coa <" .. theDuelist.coa .. ">", 30)
trigger.action.setUserFlag(dgName,100) -- anything but 0 means closed
end
end
end
end
function duel.openSlotsForZoneAndCoa(theZone, coa)
local allDuelists = theZone.duelists
for unitName, theDuelist in pairs(allDuelists) do
if (theDuelist.coa == coa) then
if duel.verbose then
trigger.action.outText("+++duel: opening SSB slot for group <" .. theDuelist.groupName .. ">, coa <" .. theDuelist.coa .. ">", 30)
trigger.action.setUserFlag(theDuelist.groupName, 0) -- 0 means OPEN
end
end
end
end
function duel.checkReopenSlotsForZoneAndCoa(theZone, coa)
-- test if one side can reopen all slots to enter the duel
-- if so, will reset FSM for zone
local allDuelists = theZone.duelists
local allUnengaged = true
for unitName, theDuelist in pairs(allDuelists) do
if (theDuelist.coa == coa) then
local theUnit = Unit.getByName(unitName)
if theUnit and Unit.isExist(theUnit) then
-- unit is still alive on this side, can't reopen
allUnengaged = false
end
end
end
if allUnengaged then
duel.openSlotsForZoneAndCoa(theZone, coa)
theZone.state = "waiting"
end
end
function duel.duelistEnteredArena(theUnit, theDuelist)
-- we connect the player with duelist slot
theDuelist.playerName = theUnit:getPlayerName()
theDuelist.active = true
local player = theUnit:getPlayerName()
local unitName = theUnit:getName()
local groupName = theDuelist.groupName
local theZone = theDuelist.zone --duel.duelZones[theDuelist.arena]
local coa = theDuelist.coa
if duel.verbose then
trigger.action.outText("Player <" .. player .. "> entered arena <" .. theZone:getName() .. "> in unit <" .. unitName .. "> of group <" .. groupName .. "> type <" .. theDuelist.type .. ">, belongs to coalition <" .. coa .. ">", 30)
end
-- close all slots for this zone and coalition
duel.closeSlotsForZoneAndCoaExceptGroupNamed(theZone, coa, groupName)
end
function duel:onEvent(event)
if not event then return end
if duel.verbose then
--trigger.action.outText("Event: " .. event.id .. " (" .. dcsCommon.event2text(event.id) .. ")", 30)
end
local theUnit = event.initiator
if not theUnit then return end
if event.id == 15 then -- birth
local unitName = theUnit:getName()
-- see if this is a duelist that has spawned
if not duel.allDuelists[unitName] then
return -- not a duelist, not my problem
end
-- unit that entered is player controlled, and duelist
duel.duelistEnteredArena(theUnit, duel.allDuelists[unitName])
end
end
--
-- update
--
function duel.update()
-- call me in a second to poll triggers
timer.scheduleFunction(duel.update, {}, timer.getTime() + 1/duel.ups)
-- find units that have disappeared, and react accordingly
for unitName, theDuelist in pairs (duel.allDuelists) do
local theZone = theDuelist.zone
if theDuelist.active then
-- trigger.action.outText("+++duel: unit <" .. unitName .. "> is active in zone <" .. theZone:getName() .. ">, controlled by <" .. theDuelist.playerName .. ">", 30)
local theUnit = Unit.getByName(unitName)
if theUnit and Unit.isExist(theUnit) then
-- all is well
else
if duel.verbose then
trigger.action.outText("+++duel: unit <" .. unitName .. "> controlled by <" .. theDuelist.playerName .. "> has disappeared, starting cleanup", 30)
end
theDuelist.playerName = nil
theDuelist.active = false
duel.checkReopenSlotsForZoneAndCoa(theZone, theDuelist.coa)
end
end
end
-- now handle FSM for each zone separately
for zoneName, theZone in pairs(duel.duelZones) do
end
end
--
-- Config & start
--
function duel.readConfigZone()
local theZone = cfxZones.getZoneByName("duelConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("duelConfig")
end
duel.verbose = theZone.verbose
duel.ups = theZone:getNumberFromZoneProperty("ups", 1)
duel.inside = theZone:getBoolFromZoneProperty("inside", true)
duel.gracePeriod = theZone:getNumberFromZoneProperty("gracePeriod", 30)
duel.keepScore = theZone:getBoolFromZoneProperty("score", true)
if duel.verbose then
trigger.action.outText("+++duel: read config", 30)
end
end
function duel.start()
if not dcsCommon.libCheck then
trigger.action.outText("cfx duel requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("cfx Duel", duel.requiredLibs) then
return false
end
-- turn on SSB
trigger.action.setUserFlag("SSB",100)
-- read config
duel.readConfigZone()
-- process cloner Zones
local attrZones = cfxZones.getZonesWithAttributeNamed("duel")
for k, aZone in pairs(attrZones) do
duel.createDuelZone(aZone) -- process attributes
duel.duelZones[aZone.name] = aZone -- add to list
end
-- connect event handler
world.addEventHandler(duel)
-- start update
duel.update()
trigger.action.outText("cfx Duel v" .. duel.version .. " started.", 30)
return true
end
if not duel.start() then
trigger.action.outText("cfx Duel aborted: missing libraries", 30)
duel = nil
end

View File

@ -1,5 +1,5 @@
guardianAngel = {}
guardianAngel.version = "3.0.4"
guardianAngel.version = "3.0.5"
guardianAngel.ups = 10
guardianAngel.name = "Guardian Angel" -- just in case someone accesses .name
guardianAngel.launchWarning = true -- detect launches and warn pilot
@ -61,7 +61,11 @@ guardianAngel.requiredLibs = {
3.0.3 - monitorItem() guards against loss of target (nil)
3.0.4 - launchSound attribute
- interventionSound attribute
3.0.5 - better missiole names, their object IDs seem to have disappeared, also storing launcher name
- msgTime to control how long warnings remain on the screen
- disappear message now only on verbose
- dmlZones
This script detects missiles launched against protected aircraft an
removes them when they are about to hit
@ -133,7 +137,7 @@ end
--
-- watch q items
--
function guardianAngel.createQItem(theWeapon, theTarget, threat)
function guardianAngel.createQItem(theWeapon, theTarget, threat, launcher)
if not theWeapon then return nil end
if not theTarget then return nil end
if not theTarget:isExist() then return nil end
@ -142,8 +146,26 @@ function guardianAngel.createQItem(theWeapon, theTarget, threat)
-- watch it for re-targeting purposes
local theItem = {}
local oName = tostring(theWeapon:getName())
if not oName or #oName < 1 then oName = dcsCommon.numberUUID() end
local wName = ""
if theWeapon.getDisplayName then
wName = theWeapon:getDisplayName() -- does this even exist any more?
elseif theWeapon.getTypeName then
wName = theWeapon:getTypeName()
else
wName = "<Generic>"
end
wName = wName .. "-" .. oName
local launcherName = launcher:getTypeName() .. " " .. launcher:getName()
theItem.theWeapon = theWeapon -- weapon that we are tracking
theItem.weaponName = theWeapon:getName()
theItem.weaponName = wName -- theWeapon:getName()
-- usually weapons have no 'name' except an ID, so let's get the
-- type or display name. Weapons often have no display name.
if guardianAngel.verbose then
trigger.action.outText("gA: tracking missile <" .. wName .. "> launched by <" .. launcherName .. ">", guardianAngel.msgTime)
end
theItem.theTarget = theTarget
theItem.tGroup = theTarget:getGroup()
theItem.tID = theItem.tGroup:getID()
@ -156,6 +178,7 @@ function guardianAngel.createQItem(theWeapon, theTarget, threat)
theItem.threat = threat
theItem.lastDesc = "(new)"
theItem.timeStamp = timer.getTime()
theItem.launcher = launcherName
return theItem
end
@ -222,14 +245,17 @@ function guardianAngel.monitorItem(theItem)
if not w then return false end
if not w:isExist() then
--if (not theItem.missed) and (not theItem.lostTrack) then
local desc = theItem.weaponName .. ": DISAPPEARED"
--[[--
if guardianAngel.announcer and theItem.threat then
local desc = theItem.weaponName .. ": DISAPPEARED"
if guardianAngel.private then
trigger.action.outTextForGroup(ID, desc, 30)
trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime)
else
trigger.action.outText(desc, 30)
trigger.action.outText(desc, guardianAngel.msgTime)
end
end
--]]--
if guardianAngel.verbose then
trigger.action.outText("+++gA: missile disappeared: <" .. theItem.weaponName .. ">, aimed at <" .. theItem.targetName .. ">",30)
end
@ -277,13 +303,13 @@ function guardianAngel.monitorItem(theItem)
if isThreat and guardianAngel.announcer and guardianAngel.active then
local desc = "Missile, missile, missile - now heading for " .. ctName .. "!"
if guardianAngel.private then
trigger.action.outTextForGroup(ID, desc, 30)
trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime)
if guardianAngel.launchSound then
local fileName = "l10n/DEFAULT/" .. guardianAngel.launchSound
trigger.action.outSoundForGroup(ID, fileName)
end
else
trigger.action.outText(desc, 30)
trigger.action.outText(desc, guardianAngel.msgTime)
if guardianAngel.launchSound then
local fileName = "l10n/DEFAULT/" .. guardianAngel.launchSound
trigger.action.outSound(fileName)
@ -340,13 +366,13 @@ function guardianAngel.monitorItem(theItem)
if guardianAngel.announcer then
if guardianAngel.private then
trigger.action.outTextForGroup(ID, desc, 30)
trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime)
if guardianAngel.interventionSound then
local fileName = "l10n/DEFAULT/" .. guardianAngel.interventionSound
trigger.action.outSoundForGroup(ID, fileName)
end
else
trigger.action.outText(desc, 30)
trigger.action.outText(desc, guardianAngel.msgTime)
if guardianAngel.interventionSound then
local fileName = "l10n/DEFAULT/" .. guardianAngel.interventionSound
trigger.action.outSound(fileName)
@ -375,9 +401,9 @@ function guardianAngel.monitorItem(theItem)
if guardianAngel.announcer then
if guardianAngel.private then
trigger.action.outTextForGroup(ID, desc, 30)
trigger.action.outTextForGroup(ID, desc, guardianAngel.msgTime)
else
trigger.action.outText(desc, 30)
trigger.action.outText(desc, guardianAngel.msgTime)
end
end
guardianAngel.invokeCallbacks("intervention", theItem.targetName, theItem.weaponName)
@ -591,6 +617,7 @@ function guardianAngel.somethingHappened(event)
-- if we get here, we have weapon aimed at a target
local targetName = theTarget:getName()
local watchedUnit = guardianAngel.getWatchedUnitByName(targetName)
local launcher = theUnit
guardianAngel.missilesAndTargets[theWeapon:getName()] = targetName
if not watchedUnit then
-- we may still want to watch this if the missile
@ -599,14 +626,14 @@ function guardianAngel.somethingHappened(event)
trigger.action.outText("+++gA: missile <" .. theWeapon:getName() .. "> targeting <" .. targetName .. ">, not a threat", 30)
end
-- add it as no threat
local theQItem = guardianAngel.createQItem(theWeapon, theTarget, false) -- this is not a threat, simply watch for re-target
local theQItem = guardianAngel.createQItem(theWeapon, theTarget, false, launcher) -- this is not a threat, simply watch for re-target
table.insert(guardianAngel.missilesInTheAir, theQItem)
return
end -- fired at some other poor sucker, we don't care
-- if we get here, someone fired a guided weapon at my watched units
-- create a new item for my queue
local theQItem = guardianAngel.createQItem(theWeapon, theTarget, true) -- this is watched
local theQItem = guardianAngel.createQItem(theWeapon, theTarget, true, launcher) -- this is watched
table.insert(guardianAngel.missilesInTheAir, theQItem)
guardianAngel.invokeCallbacks("launch", theQItem.targetName, theQItem.weaponName)
@ -624,13 +651,13 @@ function guardianAngel.somethingHappened(event)
-- currently, we always detect immediately
-- can be moved to update()
if guardianAngel.private then
trigger.action.outTextForGroup(grpID, "Missile, missile, missile, " .. oclock .. " o clock" .. vbInfo, 30)
trigger.action.outTextForGroup(grpID, "Missile, missile, missile, " .. oclock .. " o clock" .. vbInfo, guardianAngel.msgTime)
if guardianAngel.launchSound then
local fileName = "l10n/DEFAULT/" .. guardianAngel.launchSound
trigger.action.outSoundForGroup(grpID, fileName)
end
else
trigger.action.outText("Missile, missile, missile, " .. oclock .. " o clock" .. vbInfo, 30)
trigger.action.outText("Missile, missile, missile, " .. oclock .. " o clock" .. vbInfo, guardianAngel.msgTime)
if guardianAngel.launchSound then
local fileName = "l10n/DEFAULT/" .. guardianAngel.launchSound
trigger.action.outSound(fileName)
@ -847,40 +874,42 @@ function guardianAngel.readConfigZone()
end
guardianAngel.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
guardianAngel.verbose = theZone:getBoolFromZoneProperty("verbose", false)
guardianAngel.autoAddPlayer = cfxZones.getBoolFromZoneProperty(theZone, "autoAddPlayer", true)
guardianAngel.launchWarning = cfxZones.getBoolFromZoneProperty(theZone, "launchWarning", true)
guardianAngel.intervention = cfxZones.getBoolFromZoneProperty(theZone, "intervention", true)
guardianAngel.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true)
guardianAngel.private = cfxZones.getBoolFromZoneProperty(theZone, "private", false)
guardianAngel.explosion = cfxZones.getNumberFromZoneProperty(theZone, "explosion", -1)
guardianAngel.fxDistance = cfxZones.getNumberFromZoneProperty(theZone, "fxDistance", 500)
guardianAngel.autoAddPlayer = theZone:getBoolFromZoneProperty("autoAddPlayer", true)
guardianAngel.launchWarning = theZone:getBoolFromZoneProperty("launchWarning", true)
guardianAngel.intervention = theZone:getBoolFromZoneProperty("intervention", true)
guardianAngel.announcer = theZone:getBoolFromZoneProperty( "announcer", true)
guardianAngel.private = theZone:getBoolFromZoneProperty("private", false)
guardianAngel.explosion = theZone:getNumberFromZoneProperty("explosion", -1)
guardianAngel.fxDistance = theZone:getNumberFromZoneProperty( "fxDistance", 500)
guardianAngel.active = cfxZones.getBoolFromZoneProperty(theZone, "active", true)
guardianAngel.active = theZone:getBoolFromZoneProperty("active", true)
if cfxZones.hasProperty(theZone, "activate?") then
guardianAngel.activate = cfxZones.getStringFromZoneProperty(theZone, "activate?", "*<none>")
guardianAngel.lastActivate = cfxZones.getFlagValue(guardianAngel.activate, theZone)
elseif cfxZones.hasProperty(theZone, "on?") then
guardianAngel.activate = cfxZones.getStringFromZoneProperty(theZone, "on?", "*<none>")
guardianAngel.lastActivate = cfxZones.getFlagValue(guardianAngel.activate, theZone)
guardianAngel.msgTime = theZone:getNumberFromZoneProperty("msgTime", 30)
if theZone:hasProperty("activate?") then
guardianAngel.activate = theZone:getStringFromZoneProperty("activate?", "*<none>")
guardianAngel.lastActivate = theZone:getFlagValue(guardianAngel.activate)
elseif theZone:hasProperty("on?") then
guardianAngel.activate = theZone:getStringFromZoneProperty("on?", "*<none>")
guardianAngel.lastActivate = theZone:getFlagValue(guardianAngel.activate)
end
if cfxZones.hasProperty(theZone, "deactivate?") then
guardianAngel.deactivate = cfxZones.getStringFromZoneProperty(theZone, "deactivate?", "*<none>")
guardianAngel.lastDeActivate = cfxZones.getFlagValue(guardianAngel.deactivate, theZone)
elseif cfxZones.hasProperty(theZone, "off?") then
guardianAngel.deactivate = cfxZones.getStringFromZoneProperty(theZone, "off?", "*<none>")
guardianAngel.lastDeActivate = cfxZones.getFlagValue(guardianAngel.deactivate, theZone)
if theZone:hasProperty("deactivate?") then
guardianAngel.deactivate = theZone:getStringFromZoneProperty("deactivate?", "*<none>")
guardianAngel.lastDeActivate = theZone:getFlagValue(guardianAngel.deactivate)
elseif theZone:hasProperty("off?") then
guardianAngel.deactivate = theZone:getStringFromZoneProperty("off?", "*<none>")
guardianAngel.lastDeActivate = theZone:getFlagValue(guardianAngel.deactivate)
end
if cfxZones.hasProperty(theZone, "launchSound") then
guardianAngel.launchSound = cfxZones.getStringFromZoneProperty(theZone, "launchSound", "nosound")
if theZone:hasProperty("launchSound") then
guardianAngel.launchSound = theZone:getStringFromZoneProperty("launchSound", "nosound")
end
if cfxZones.hasProperty(theZone, "interventionSound") then
guardianAngel.interventionSound = cfxZones.getStringFromZoneProperty(theZone, "interventionSound", "nosound")
if theZone:hasProperty("interventionSound") then
guardianAngel.interventionSound = theZone:getStringFromZoneProperty("interventionSound", "nosound")
end
guardianAngel.configZone = theZone
@ -894,7 +923,7 @@ end
--
function guardianAngel.processGuardianZone(theZone)
theZone.angelic = cfxZones.getBoolFromZoneProperty(theZone, "guardian", true)
theZone.angelic = theZone:getBoolFromZoneProperty("guardian", true)
if theZone.verbose or guardianAngel.verbose then
@ -947,7 +976,7 @@ function guardianAngel.start()
end
function guardianAngel.testCB(reason, targetName, weaponName)
trigger.action.outText("gA - CB for ".. reason .. ": " .. targetName .. " w: " .. weaponName, 30)
trigger.action.outText("gA - CB for ".. reason .. ": " .. targetName .. " w: " .. weaponName, guardianAngel.msgTime)
end
-- go go go

View File

@ -1,5 +1,5 @@
unitZone={}
unitZone.version = "1.2.4"
unitZone.version = "1.2.5"
unitZone.verbose = false
unitZone.ups = 1
unitZone.requiredLibs = {
@ -17,6 +17,7 @@ unitZone.requiredLibs = {
1.2.3 - better guards for enterZone!, exitZone!, changeZone!
- better guards for uzOn? and uzOff?
1.2.4 - more verbosity on uzDirect
1.2.5 - reading config improvement
--]]--
@ -196,8 +197,9 @@ function unitZone.checkZoneStatus(theZone)
local playerCheck = theZone.matching == "player"
if playerCheck then
-- we check the names for players only
-- collector holds units, not groups
for idx, pUnit in pairs(theGroups) do
local puName=pUnit:getName()
local puName = pUnit:getName()
local hasMatch = false
if theZone.lookForBeginsWith then
hasMatch = dcsCommon.stringStartsWith(puName, lookFor)
@ -323,10 +325,7 @@ end
function unitZone.readConfigZone()
local theZone = cfxZones.getZoneByName("unitZoneConfig")
if not theZone then
if unitZone.verbose then
trigger.action.outText("+++uZne: NO config zone!", 30)
end
return
theZone = cfxZones.createSimpleZone("unitZoneConfig")
end
unitZone.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)