DML/modules/civAir.lua
Christian Franz 38d6487de7 Version 2.0.1
Updates to Debugger, csarManager, PlayerScore/objectDestructDetector
2024-01-25 08:12:29 +01:00

774 lines
27 KiB
Lua

civAir = {}
civAir.version = "3.0.2"
--[[--
3.0.0 liveries support
default liveries for Yak-50 (main test case)
default liveries for C-130, c-17A, IL-76MD, An-30M, An-26B
default aircraft types dcs
support for CAM
default liveries for all CAM types
new DCS attribute
new CAM attribute
deafault to one Yak-40 if neither
support for 'civil_liveries' zone
3.0.1 protest option, on by default
protest action
spawning now works correctly for groupType
3.0.2 clean-up
--]]--
civAir.ups = 0.05 -- updates per second. 0.05 = once every 20 seconds
civAir.initialAirSpawns = true -- when true has population spawn in-air at start
civAir.verbose = false
civAir.aircraftTypes = {}
civAir.dcsBuiltinTypes = {"Yak-40", "C-130", "C-17A", "IL-76MD", "An-30M", "An-26B"}
civAir.CAMTypes = { "A_320", "A_330", "A_380", "B_727", "B_737", "B_747", "B_757", "Cessna_210N", "DC_10",}
civAir.liveries = {
-- definitions for plain vanilla DCS
["Yak-40"] = {"Aeroflot", "Algeria GLAM", "Olympic Airways", "Ukranian", "Georgian Airlines", }, --"Georgian Airlines",
["C-130"] = {"Air Algerie L-382 White", "Algerian AF Green", "Algerian AF H30 White", "Belgian Air Force", "Canada's Air Force", "French Air Force", "HAF gray", "IRIAF 5-8503", "IRIAF 5-8518", "Israel Defence Force", "Royal Air Force", "Royal Danish Air Force", "Royal Netherlands Air Force", "Royal Norwegian Air Force", "Spanish Air Force", "Turkish Air Force", "US Air Force", },
["C-17A"] = {"usaf standard", },
["IL-76MD"] = {"Algerian AF IL-76MD", "China Air Force New", "China Air Force Old", "FSB aeroflot", "MVD aeroflot", "RF Air Force", "Ukrainian AF", "Ukrainian AF aeroflot", },
["An-30M"] = {"15th Transport AB", "China CAAC", "RF Air Force"},
["An-26B"] = {"Abkhazian AF", "Aeroflot", "China PLAAF", "Georgian AF", "RF Air Force", "RF Navy", "Ukraine AF", },
-- definitions for CAM mod
["A_320"] = {"Aeroflot", "Aeroflot 1", "Air Asia", "Air Berlin", "Air Berlin FFO", "Air Berlin OLT", "Air Berlin retro", "Air France", "Air Moldova", "Airbus Neo", "Al Maha", "Alitalia", "American Airlines", "British Airways", "Cebu Pacific", "Clean", "Condor", "Delta Airlines", "Easy Jet", "Easy Jet Berlin", "Easy Jet w", "Edelweiss", "Emirates", "Etihad", "Eurowings", "Eurowings BVB09", "Eurowings Europa Park", "Fly Georgia", "Fly Niki", "Frontier", "German Wings", "Gulf Air", "Iberia", "Iran Air", "Jet Blue NY", "JetBlue", "jetBlue FDNY", "Kish Air", "Kuwait Airways", "Lufthansa", "Lufthansa New", "MEA", "MRTT Canada", "MRTT Luftwaffe", "Qatar", "RAF MPA", "RAF VIP", "S7", "SAS", "Saudi Gulf", "Saudia", "Small Planet", "Star Alliance", "SWISS", "Thomas Cook", "Tunis Air", "Turkish Airlines", "United", "Ural Airlines", "US Airways", "Vietnam Airlines", "Virgin", "WiZZ", "WiZZ Budapest", "WOW", },
["A_330"] = {"Aer Lingus", "Aeroflot", "Air Canada", "Air China", "Air Tahiti Nui", "AirAsia", "Airbus", "BOURKHAN", "Brussels Airline", "Cathay Pacific", "CEBU Pacific", "China Eastern", "Clean", "DELTA", "DragonAir", "Edelweiss", "Egypt Air", "Emirates", "ETIHAD", "EVA", "FIJI", "FinnAir", "FrenchBlue", "Garude Indunesia", "GulfAir", "Hainan Airlines", "Iberia", "IRoI", "KLM", "LAN Airways", "Lion Air PK-LEG", "LTU", "Lufthansa", "NWA", "nwaold", "Olympic", "OmanAir", "Orbit", "Philipines", "Qantas", "Qatar", "RAF Voyager", "Singapore", "Skyteam", "Srilankan", "Star Aliance", "Swiss", "Thomas Cook", "Turkish Airlines", "US Airways", "Virgin Atlantic", "WorldTrave", },
["A_380"] = {"Air France", "BA", "China Southern", "Clean", "Emirates", "KA", "LH", "LHF", "Qantas Airways", "QTR", "SA", "TA", },
["B_727"] = {"AEROFLOT", "Air France", "Alaska", "Alitalia", "American Airlines", "Clean", "Delta Airlines", "Delta Airlines OLD", "FedEx", "Hapag Lloyd", "Lufthansa", "Lufthansa Oberhausen Old", "Northwest", "Pan Am", "Singapore Airlines", "Southwest", "UNITED", "UNITED Old", "ZERO G", },
["B_737"] = {"Air Algerie", "Air Berlin", "Air France", "airBaltic", "Airzena", "AM", "American_Airlines", "British Airways", "C40s", "Clean", "Disney", "EA", "easyJet", "FINNAIR", "HARIBO", "JA", "Jet2", "kulula", "LH", "Lufthansa BA", "Lufthansa KR", "OLD_BA", "OMAN AIR", "P8 RAF", "P8 USN", "PAN AM", "Polskie Linie Lotnicze LOT", "QANTAS", "RYANAIR", "SouthWest Lone Star", "ThomsonFly", "TNT", "Ukraine Airlines", "UPS", },
["B_747"] = {"AF", "AF-One", "AI", "CP", "IM", "KLM", "LH", "NW", "PA", "QA", "TA", },
["B_757"] = {"AA", "BA", "C-32", "Delta", "DHL", "easyJet", "Swiss", "Thomson", },
["Cessna_210N"] = {"Blank", "D-EKVW", "HellenicAF", "Muster", "N9572H", "SEagle blue", "SEagle red", "USAF-Academy", "V5-BUG", "VH-JGA", },
["DC_10"] = {"SWISSAIR HB-IHL", "SWISSAIR HB-IMC", "SWISSAIR HB-IPF", }
}
civAir.maxTraffic = 10 -- number of flights at the same time
civAir.maxIdle = 8 * 60 -- seconds of ide time before it is removed after landing
civAir.trafficCenters = {}
civAir.excludeAirfields = {}
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
"cfxZones", -- zones management foc CSAR and CSAR Mission zones
}
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
theZone = cfxZones.createSimpleZone("civAirConfig")
end
civAir.verbose = theZone.verbose
civAir.ups = theZone:getNumberFromZoneProperty("ups", 0.05)
if civAir.ups < .0001 then civAir.ups = 0.05 end
if theZone:hasProperty("maxTraffic") then
civAir.maxTraffic = theZone:getNumberFromZoneProperty( "maxTraffic", 10)
else --if theZone:hasProperty("maxFlights") then
civAir.maxTraffic = theZone:getNumberFromZoneProperty( "maxFlights", 10)
end
civAir.maxIdle = theZone:getNumberFromZoneProperty("maxIdle", 8 * 60)
civAir.initialAirSpawns = theZone:getBoolFromZoneProperty( "initialAirSpawns", true)
civAir.owner = theZone:getNumberFromZoneProperty("owner", 82) -- default to UN peacekeepers
-- build my aircraft types list
local hasDCS = theZone:getBoolFromZoneProperty("dcs", true)
if hasDCS then
if civAir.verbose then trigger.action.outText("+++civA: adding DCS standard types", 30) end
for idx, aType in pairs(civAir.dcsBuiltinTypes) do
table.insert(civAir.aircraftTypes, aType)
end
end
local hasCAM = theZone:getBoolFromZoneProperty("cam", false)
if hasCAM then
if civAir.verbose then trigger.action.outText("+++civA: adding CAM add-on types", 30) end
for idx, aType in pairs(civAir.CAMTypes) do
table.insert(civAir.aircraftTypes, aType)
end
end
-- now get types and liveries from 'civil_liveries' if present
local livZone = cfxZones.getZoneByName("civil_liveries")
if livZone then
if civAir.verbose then
trigger.action.outText("civA: found and processing 'civil_liveries' zone data.", 30)
end
-- read all into my types registry, replacing whatever is there
local rawLiver = cfxZones.getAllZoneProperties(livZone)
local newTypes, newLiveries = civAir.addTypesAndLiveries(rawLiver)
-- now types to existing types if not already there
for idx, aType in pairs(newTypes) do
dcsCommon.addToTableIfNew(civAir.aircraftTypes, aType)
if civAir.verbose then
trigger.action.outText("+++civA: processed and added aircraft <" .. aType .. "> to civAir", 30)
end
end
-- now replace liveries or add if not already there
for aType, liveries in pairs(newLiveries) do
civAir.liveries[aType] = liveries
if civAir.verbose then
trigger.action.outText("+++civA: replaced/added liveries for aircraft <" .. aType .. ">", 30)
end
end
end
if #civAir.aircraftTypes < 1 then
table.insert(civAir.aircraftTypes, "Yak-40")
if civAir.verbose then
trigger.action.outText("+++civA: adding singular Yak-40", 30)
end
end
-- selective types, overwrites existing types when present
-- also provides legacy support
if theZone:hasProperty("aircraftTypes") then
local theTypes = theZone:getStringFromZoneProperty( "aircraftTypes", civAir.aircraftTypes)
local typeArray = dcsCommon.splitString(theTypes, ",")
typeArray = dcsCommon.trimArray(typeArray)
civAir.aircraftTypes = typeArray
if civAir.verbose then
trigger.action.outText("+++civA: setting aircraft types to <" .. theTypes .. ">", 30)
end
end
civAir.protest = theZone:getBoolFromZoneProperty("protest", true)
end
function civAir.addTypesAndLiveries(rawIn)
local newTypes = {}
local newLiveries = {}
-- now iterate the input table, and generate new types and
-- liveries from it
for theType, liveries in pairs (rawIn) do
if civAir.verbose then
trigger.action.outText("+++civA: processing type <" .. theType .. ">:<" .. liveries .. ">", 30)
end
local livA = dcsCommon.splitString(liveries, ',')
livA = dcsCommon.trimArray(livA)
table.insert(newTypes, theType)
newLiveries[theType] = livA
end
return newTypes, newLiveries
end
function civAir.processZone(theZone)
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" or value == "closed" then
table.insert(civAir.excludeAirfields, afName)
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
elseif dcsCommon.stringStartsWith(value, "outb") then
table.insert(civAir.landingOnly, inoutName)
civAir.inoutZones[inoutName] = theZone
elseif dcsCommon.stringStartsWith(value, "in/out") then
table.insert(civAir.trafficCenters, inoutName)
civAir.inoutZones[inoutName] = theZone
else
table.insert(civAir.trafficCenters, afName) -- note that adding the same twice makes it more likely to be picked
end
else
trigger.action.outText("+++civA: unable to resolve airfield for <" .. theZone.name .. ">", 30)
end
end
function civAir.addPlane(thePlaneUnit) -- warning: is actually a group
if not thePlaneUnit then return end
civAir.activePlanes[thePlaneUnit:getName()] = thePlaneUnit
end
function civAir.removePlaneGroupByName(aName)
if not aName then
return
end
if civAir.activePlanes[aName] then
civAir.activePlanes[aName] = nil
else
if civAir.verbose then
trigger.action.outText("civA: warning - ".. aName .." remove req but not found", 30)
end
end
end
function civAir.removePlane(thePlaneUnit) -- warning: is actually a group
if not thePlaneUnit then return end
if not thePlaneUnit:isExist() then return end
civAir.activePlanes[thePlaneUnit:getName()] = nil
end
function civAir.getPlane(aName) -- warning: returns GROUP!
return civAir.activePlanes[aName]
end
function civAir.filterAirfields(inAll, inFilter)
local outList = {}
for idx, anItem in pairs(inAll) do
if dcsCommon.arrayContainsString(inFilter, anItem) then
-- filtered, do nothing.
else
-- not filtered
table.insert(outList, anItem)
end
end
return outList
end
function civAir.getTwoAirbases()
local fAB -- first airbase to depart
local sAB -- second airbase to fly to
local departAB = dcsCommon.combineTables(civAir.trafficCenters, civAir.departOnly)
-- remove all currently excluded air bases from departure
local filteredAB = civAir.filterAirfields(departAB, civAir.excludeAirfields)
-- if none left, error
if #filteredAB < 1 then
trigger.action.outText("+++civA: too few departure airfields", 30)
return nil, nil
end
-- now pick the departure airfield
fAB = dcsCommon.pickRandom(filteredAB)
-- now generate list of landing airfields
local arriveAB = dcsCommon.combineTables(civAir.trafficCenters, civAir.landingOnly)
-- remove all currently excluded air bases from arrival
filteredAB = civAir.filterAirfields(arriveAB, civAir.excludeAirfields)
-- if one left use it twice, boring flight.
if #filteredAB < 1 then
trigger.action.outText("+++civA: too few arrival airfields", 30)
return nil, nil
end
-- pick any second that are not the same
local tries = 0
repeat
sAB = dcsCommon.pickRandom(filteredAB)
tries = tries + 1 -- only try 10 times
until fAB ~= sAB or tries > 10
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 civA, civB -- fAB, sAB
end
function civAir.parkingIsFree(fromWP)
-- iterate over all currently registred flights and make
-- sure that their location isn't closer than 10m to my new parking
local loc = {}
loc.x = fromWP.x
loc.y = fromWP.alt
loc.z = fromWP.z
for name, aPlaneGroup in pairs(civAir.activePlanes) do
if Group.isExist(aPlaneGroup) then
local aPlane = aPlaneGroup:getUnit(1)
if aPlane and Unit.isExist(aPlane) then
pos = aPlane:getPoint()
local delta = dcsCommon.dist(loc, pos)
if delta < 21 then
-- way too close
trigger.action.outText("civA: too close for comfort - " .. aPlane:getName() .. " occupies my slot", 30)
return false
end
end
end
end
return true
end
civAir.airStartSeparation = 0
function civAir.createFlight(name, theTypeString, fromAirfield, toAirfield, inAirStart)
if not fromAirfield then
trigger.action.outText("civA: NIL source", 30)
return nil
end
if not toAirfield then
trigger.action.outText("civA: NIL destination", 30)
return nil
end
local randomizeLoc = inAirStart
local theGroup = dcsCommon.createEmptyAircraftGroupData (name)
local theAUnit = dcsCommon.createAircraftUnitData(name .. "-GA", theTypeString, false)
-- add livery capability for this aircraft
civAir.processLiveriesFor(theAUnit, theTypeString)
-- enforce civ attribute
theAUnit.civil_plane = true
theAUnit.payload.fuel = 100000
dcsCommon.addUnitToGroupData(theAUnit, theGroup)
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
end
if inAirStart then
-- modify WP into an in-air point
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
-- 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)
if not zoneApproach then
dcsCommon.addRoutePointForGroupData(theGroup, overheadWP)
end
dcsCommon.addRoutePointForGroupData(theGroup, toWP)
-- spawn
local groupCat = Group.Category.AIRPLANE
local theSpawnedGroup = coalition.addGroup(civAir.owner, groupCat, theGroup) -- 82 is UN peacekeepers
if zoneApproach then
-- track this flight to target zone
civAir.outboundFlights[name] = zoneApproach
end
return theSpawnedGroup
end
-- flightCount is a global that holds the number of flights we track
civAir.flightCount = 0
function civAir.createNewFlight(inAirStart)
civAir.flightCount = civAir.flightCount + 1
local fAB, sAB = civAir.getTwoAirbases() -- from AB
if not fAB or not sAB then
trigger.action.outText("+++civA: cannot create flight, no source or destination", 30)
return
end
-- 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)
if not theFlight then
-- flight was not able to spawn.
trigger.action.outText("civA: aborted civ spawn on fAB:" .. fAB:getName(), 30)
return
end
civAir.addPlane(theFlight) -- track it
if civAir.verbose then
trigger.action.outText("civA: created flight from <" .. fAB.name .. "> to <" .. sAB.name .. ">", 30)
end
end
function civAir.airStartPopulation()
local numAirStarts = civAir.maxTraffic / 2
civAir.airStartSeparation = 0
while numAirStarts > 0 do
numAirStarts = numAirStarts - 1
civAir.airStartSeparation = civAir.airStartSeparation + 200
civAir.createNewFlight(true)
end
-- start update in 15 seconds
timer.scheduleFunction(civAir.update, {}, timer.getTime() + 15)
end
--
-- Livery handling
--
function civAir.processLiveriesFor(theData, theType)
if civAir.liveries[theType] then
local available = civAir.liveries[theType]
local chosen = dcsCommon.pickRandom(available)
theData.livery_id = chosen
end
end
--
-- 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)
-- clean-up first:
-- any group that no longer exits will be removed from the array
local removeMe = {}
for name, group in pairs (civAir.activePlanes) do
if not group:isExist() then
table.insert(removeMe, name) -- mark for deletion
end
end
for idx, name in pairs(removeMe) do
civAir.activePlanes[name] = nil
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()
for name, aPlaneGroup in pairs(civAir.activePlanes) do
local speed = 0
if aPlaneGroup:isExist() then
local aPlane = aPlaneGroup:getUnit(1)
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, plane no longer exists
civAir.idlePlanes[name] = -1000
speed = 0
end
else
-- force removal, group no longer exists
civAir.idlePlanes[name] = -1000
speed = 0
end
if speed < 0.5 then
if not civAir.idlePlanes[name] then
civAir.idlePlanes[name] = now
end
local idleTime = now - civAir.idlePlanes[name]
--trigger.action.outText("civA: Idling <" .. name .. "> for t=" .. idleTime, 30)
if idleTime > civAir.maxIdle then
table.insert(overduePlanes, name)
end
else
-- zero out idle plane, it's moving fast enough
civAir.idlePlanes[name] = nil
end
end
-- see if we have less than max flights running
if planeNum < civAir.maxTraffic then
-- spawn a new plane. just one per pass
civAir.createNewFlight()
end
-- now remove all planes that are overdue
for idx, aName in pairs(overduePlanes) do
local aFlight = civAir.getPlane(aName) -- returns a group
civAir.removePlaneGroupByName(aName) -- remove from roster
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
--
-- onEvent: detect hits / kills
--
function civAir:onEvent(event)
if not civAir.protest then return end
if not event.initiator then return end
local theUnit = event.initiator
if not Unit.isExist(theUnit) then return end
if event.id == 28 then -- kill event
-- check if the unit that was willed is one of mine
local target = event.target
if not target then return end
if not target.getGroup then return end
local theGroup = target:getGroup()
if not theGroup then return end
local theName = theGroup:getName()
-- see if theName matches one of my flights
local theFlight = civAir.activePlanes[theName]
if not theFlight then return end
-- if we get here, a civ plane got killed
if not theUnit.getPlayerName then return end
local thePlayer = theUnit:getPlayerName()
if not thePlayer then return end
-- now protest!
local details = ""
if event.weapon and event.weapon:getTypeName() then
details = " was attacked with a < .. event.weapon.getTypeName() .. > and"
end
trigger.action.outText("\n======== N E W S F L A S H ========\nUnarmed civilian flight <" .. theName .. ">" .. details .. " has become a victim of war crime. Sadly, all lives on board of the civil flight were lost.\n\nArmed Forced pilot <" .. thePlayer .. "> and their <" .. theUnit:getTypeName() .. "> were reported lethally armed and weapons hot in the same area; <" .. thePlayer .. "> is ordered to remand to base immediately, pending court-martial.\n\n====== E N D M E S S A G E ======\n", 30)
end
end
--
-- misc stuff
--
function civAir.doDebug(any)
trigger.action.outText("cf/x civTraffic debugger.", 30)
local desc = "Active Planes:"
local now = timer.getTime()
for name, group in pairs (civAir.activePlanes) do
desc = desc .. "\n" .. name
if civAir.idlePlanes[name] then
delay = now - civAir.idlePlanes[name]
desc = desc .. " (idle for " .. delay .. ")"
end
end
trigger.action.outText(desc, 30)
end
function civAir.collectHubs()
local pZones = cfxZones.zonesWithProperty("civAir")
for k, aZone in pairs(pZones) do
civAir.processZone(aZone)
end
end
function civAir.listTrafficCenters()
trigger.action.outText("Traffic Centers", 30)
for idx, aName in pairs(civAir.trafficCenters) do
trigger.action.outText(aName, 30)
end
if #civAir.departOnly > 0 then
trigger.action.outText("Departure-Only:", 30)
for idx, aName in pairs(civAir.departOnly) do
trigger.action.outText(aName, 30)
end
end
if #civAir.landingOnly > 0 then
trigger.action.outText("Arrival/Landing-Only:", 30)
for idx, aName in pairs(civAir.landingOnly) do
trigger.action.outText(aName, 30)
end
end
end
-- start
function civAir.start()
-- module check
if not dcsCommon.libCheck("cfx civAir", civAir.requiredLibs) then
return false
end
-- see if there is a config zone and load it
civAir.readConfigZone()
-- look for zones to add to air fields list
civAir.collectHubs()
-- make sure there is something in trafficCenters
if (#civAir.trafficCenters + #civAir.departOnly < 1) or
(#civAir.trafficCenters + #civAir.landingOnly < 1)
then
trigger.action.outText("+++civA: auto-populating", 30)
-- simply add airfields on the map
local allBases = dcsCommon.getAirbasesWhoseNameContains("*", 0)
for idx, aBase in pairs(allBases) do
local afName = aBase:getName()
table.insert(civAir.trafficCenters, afName)
end
end
if civAir.verbose then
civAir.listTrafficCenters()
end
-- air-start half population if allowed
-- allow mission 15 seconds to settle before we start populating to
-- allow better access to liveries
if civAir.initialAirSpawns then
timer.scheduleFunction(civAir.airStartPopulation, {}, timer.getTime() + 5)
else
-- start update in 15 seconds
timer.scheduleFunction(civAir.update, {}, timer.getTime() + 15)
end
-- start outbound tracking
civAir.trackOutbound()
-- sign up for events
world.addEventHandler(civAir)
-- say hi!
trigger.action.outText("cf/x civAir v" .. civAir.version .. " started.", 30)
return true
end
if not civAir.start() then
trigger.action.outText("cf/x civAir aborted: missing libraries", 30)
civAir = nil
end
--[[--
Additional ideas
- callbacks for civ spawn / despawn
- add civkill callback / redCivKill blueCivKill flag bangers
- Helicopter support
- 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
--]]--