mirror of
https://github.com/weyne85/DML.git
synced 2025-10-29 16:57:49 +00:00
572 lines
18 KiB
Lua
572 lines
18 KiB
Lua
civAir = {}
|
|
civAir.version = "1.4.0"
|
|
--[[--
|
|
1.0.0 initial version
|
|
1.1.0 exclude list for airfields
|
|
1.1.1 bug fixes with remove flight
|
|
and betweenHubs
|
|
check if slot is really free before spawning
|
|
add overhead waypoint
|
|
1.1.2 inAir start possible
|
|
1.2.0 civAir can use own config file
|
|
1.2.1 slight update to config file (moved active/idle)
|
|
1.3.0 add ability to use zones to add closest airfield to
|
|
trafficCenters or excludeAirfields
|
|
1.4.0 ability to load config from zone to override
|
|
all configs it finds
|
|
module check
|
|
removed obsolete civAirConfig module
|
|
|
|
|
|
--]]--
|
|
|
|
civAir.ups = 0.05 -- updates per second
|
|
civAir.initialAirSpawns = true -- when true has population spawn in-air at start
|
|
civAir.verbose = false
|
|
|
|
-- aircraftTypes contains the type names for the neutral air traffic
|
|
-- each entry has the same chance to be chose, so to make an
|
|
-- aircraft more probably to appear, add its type multiple times
|
|
-- like here with the Yak-40
|
|
civAir.aircraftTypes = {"Yak-40", "Yak-40", "C-130", "C-17A", "IL-76MD", "An-30M", "An-26B"} -- civilian planes type strings as described here https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB
|
|
|
|
-- maxTraffic is the number of neutral flights that are
|
|
-- concurrently under way
|
|
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.trafficAirbases = {
|
|
randomized = 0, -- between any on map
|
|
localHubs = 1, -- between any two airfields inside the same random hub listed in trafficCenters
|
|
betweenHubs = 2 -- between any in random hub 1 to any in random hub 2
|
|
}
|
|
|
|
civAir.trafficRange = 100 -- 120000 -- defines hub size, in meters. Make it 100 to make it only that airfield
|
|
-- ABPickmethod determines how airfields are picked
|
|
-- for air traffic
|
|
civAir.ABPickMethod = civAir.trafficAirbases.betweenHubs
|
|
civAir.trafficCenters = {
|
|
--"batu",
|
|
--"kobul",
|
|
--"senaki",
|
|
--"kutai",
|
|
} -- trafficCenters is used with hubs. Each entry defines a hub
|
|
-- where we collect airdromes etc based on range
|
|
-- simply add a string to identify the hub center
|
|
-- e.g. "senak" to define "Senaki Kolkhi"
|
|
-- to have planes only fly between airfields in 100 km range
|
|
-- around senaki kolkhi, enter only senaki as traffic center, set
|
|
-- trafficRange to 100000 and ABPickMethod to localHubs
|
|
-- to have traffic only between any airfields listed
|
|
-- in trafficCenters, set trafficRange to a small value
|
|
-- like 100 meters and set ABPickMethod to betweenHubs
|
|
-- to have flights that always cross the map with multiple
|
|
-- airfields, choose two or three hubs that are 300 km apart,
|
|
-- then set trafficRange to 150000 and ABPickMethod to betweenHubs
|
|
-- you can also place zones on the map and add a
|
|
-- civAir attribute. If the attribute 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 by all
|
|
-- airfields in the map
|
|
|
|
civAir.excludeAirfields = {
|
|
--"senaki",
|
|
}
|
|
-- 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
|
|
-- can be populated by zone on the map that have the
|
|
-- 'civAir' attribute with value "exclude"
|
|
|
|
civAir.requiredLibs = {
|
|
"dcsCommon", -- common is of course needed for everything
|
|
"cfxZones", -- zones management foc CSAR and CSAR Mission zones
|
|
}
|
|
|
|
civAir.activePlanes = {}
|
|
civAir.idlePlanes = {}
|
|
|
|
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
|
|
end
|
|
|
|
trigger.action.outText("civA: found config zone!", 30)
|
|
|
|
-- ok, for each property, load it if it exists
|
|
if cfxZones.hasProperty(theZone, "aircraftTypes") then
|
|
civAir.aircraftTypes = cfxZones.getStringFromZoneProperty(theZone, "aircraftTypes", "Yak-40")
|
|
end
|
|
|
|
if cfxZones.hasProperty(theZone, "ups") then
|
|
civAir.ups = cfxZones.getNumberFromZoneProperty(theZone, "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)
|
|
end
|
|
|
|
if cfxZones.hasProperty(theZone, "maxIdle") then
|
|
civAir.maxIdle = cfxZones.getNumberFromZoneProperty(theZone, "maxIdle", 8 * 60)
|
|
end
|
|
|
|
if cfxZones.hasProperty(theZone, "trafficRange") then
|
|
civAir.trafficRange = cfxZones.getNumberFromZoneProperty(theZone, "trafficRange", 120000) -- 120 km
|
|
end
|
|
|
|
if cfxZones.hasProperty(theZone, "ABPickMethod") then
|
|
civAir.ABPickMethod = cfxZones.getNumberFromZoneProperty(theZone, "ABPickMethod", 0) -- randomized any
|
|
end
|
|
|
|
if cfxZones.hasProperty(theZone, "initialAirSpawns") then
|
|
civAir.initialAirSpawns = cfxZones.getBoolFromZoneProperty(theZone, "initialAirSpawns", true)
|
|
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
|
|
--trigger.action.outText("civA: REMOVING " .. aName .. " ***", 30)
|
|
civAir.activePlanes[aName] = nil
|
|
else
|
|
trigger.action.outText("civA: warning - ".. aName .." remove req but not found", 30)
|
|
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
|
|
|
|
-- get an air base, may exclude an airbase from choice
|
|
-- method is dependent on
|
|
function civAir.getAnAirbase(excludeThisOne)
|
|
-- different methods to select a base
|
|
-- purely random from current list
|
|
local theAB;
|
|
if civAir.ABPickMethod == civAir.trafficAirbases.randomized then
|
|
repeat
|
|
local allAB = dcsCommon.getAirbasesWhoseNameContains("*", 0) -- all airfields, no Ships nor FABS
|
|
theAB = dcsCommon.pickRandom(allAB)
|
|
until theAB ~= excludeThisOne
|
|
return theAB
|
|
end
|
|
|
|
if civAir.ABPickMethod == civAir.trafficAirbases.localHubs then
|
|
-- first, pick a hub name
|
|
end
|
|
|
|
trigger.action.outText("civA: warning - unknown method <" .. civAir.ABPickMethod .. ">", 30)
|
|
return nil
|
|
end
|
|
|
|
function civAir.excludeAirbases(inList, excludeList)
|
|
if not inList then return {} end
|
|
if not excludeList then return inList end
|
|
if #excludeList < 1 then return inList end
|
|
|
|
local theDict = {}
|
|
-- build dict
|
|
for idx, aBase in pairs(inList) do
|
|
theDict[aBase:getName()] = aBase
|
|
end
|
|
|
|
-- now iterate through all excludes and remove them from dics
|
|
for idx, aName in pairs (excludeList) do
|
|
local allOfflimitAB = dcsCommon.getAirbasesWhoseNameContains(aName, 0)
|
|
for idx2, illegalBase in pairs (allOfflimitAB) do
|
|
theDict[illegalBase:getName()] = nil
|
|
end
|
|
end
|
|
-- now linearise (make array) from dict
|
|
local theArray = dcsCommon.enumerateTable(theDict)
|
|
return theArray
|
|
end
|
|
|
|
function civAir.getTwoAirbases()
|
|
local fAB
|
|
local sAB
|
|
-- get any two airbases on the map
|
|
if civAir.ABPickMethod == civAir.trafficAirbases.randomized then
|
|
local allAB = dcsCommon.getAirbasesWhoseNameContains("*", 0) -- all airfields, no Ships nor FABS, all coalitions
|
|
-- remove illegal source/dest airfields
|
|
allAB = civAir.excludeAirbases(allAB, civAir.excludeAirfields)
|
|
|
|
fAB = dcsCommon.pickRandom(allAB)
|
|
repeat
|
|
sAB = dcsCommon.pickRandom(allAB)
|
|
until fAB ~= sAB or (#allAB < 2)
|
|
return fAB, sAB
|
|
end
|
|
|
|
-- pick a hub, and then selct any two different airbases in the hub
|
|
if civAir.ABPickMethod == civAir.trafficAirbases.localHubs then
|
|
local hubName = dcsCommon.pickRandom(civAir.trafficCenters)
|
|
-- get the airfield that is identified by this
|
|
local theHub = dcsCommon.getFirstAirbaseWhoseNameContains(hubName, 0) -- only airfields, all coalitions
|
|
-- get all airbases that surround in range
|
|
local allAB = dcsCommon.getAirbasesInRangeOfAirbase(
|
|
theHub, -- centered on this base
|
|
true, -- include hub itself
|
|
civAir.trafficRange, -- hub size in meters
|
|
0 -- only airfields
|
|
)
|
|
allAB = civAir.excludeAirbases(allAB, civAir.excludeAirfields)
|
|
fAB = dcsCommon.pickRandom(allAB)
|
|
repeat
|
|
sAB = dcsCommon.pickRandom(allAB)
|
|
until fAB ~= sAB or (#allAB < 2)
|
|
return fAB, sAB
|
|
end
|
|
|
|
-- pick two hubs: one for source, one for destination airfields,
|
|
-- then pick an airfield from each hub
|
|
if civAir.ABPickMethod == civAir.trafficAirbases.betweenHubs then
|
|
--trigger.action.outText("between", 30)
|
|
local sourceHubName = dcsCommon.pickRandom(civAir.trafficCenters)
|
|
--trigger.action.outText("picked " .. sourceHubName, 30)
|
|
local sourceHub = dcsCommon.getFirstAirbaseWhoseNameContains(sourceHubName, 0)
|
|
--trigger.action.outText("sourceHub " .. sourceHub:getName(), 30)
|
|
|
|
local destHub
|
|
repeat destHubName = dcsCommon.pickRandom(civAir.trafficCenters)
|
|
until destHubName ~= sourceHubName or #civAir.trafficCenters < 2
|
|
destHub = dcsCommon.getFirstAirbaseWhoseNameContains(destHubName, 0)
|
|
--trigger.action.outText("destHub " .. destHub:getName(), 30)
|
|
local allAB = dcsCommon.getAirbasesInRangeOfAirbase(
|
|
sourceHub, -- centered on this base
|
|
true, -- include hub itself
|
|
civAir.trafficRange, -- hub size in meters
|
|
0 -- only airfields
|
|
)
|
|
allAB = civAir.excludeAirbases(allAB, civAir.excludeAirfields)
|
|
fAB = dcsCommon.pickRandom(allAB)
|
|
allAB = dcsCommon.getAirbasesInRangeOfAirbase(
|
|
destHub, -- centered on this base
|
|
true, -- include hub itself
|
|
civAir.trafficRange, -- hub size in meters
|
|
0 -- only airfields
|
|
)
|
|
allAB = civAir.excludeAirbases(allAB, civAir.excludeAirfields)
|
|
sAB = dcsCommon.pickRandom(allAB)
|
|
return fAB, sAB
|
|
end
|
|
|
|
|
|
trigger.action.outText("civA: warning - unknown method <" .. civAir.ABPickMethod .. "> in getTwoAirbases()", 30)
|
|
end
|
|
|
|
function civAir.parkingIsFree(fromWP)
|
|
-- iterate over all currently registres 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 aPlaneGroup:isExist() then
|
|
local aPlane = aPlaneGroup:getUnit(1)
|
|
if aPlane:isExist() 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 fromAirfield", 30)
|
|
return nil
|
|
end
|
|
|
|
if not toAirfield then
|
|
trigger.action.outText("civA: NIL toAirfield", 30)
|
|
return nil
|
|
end
|
|
|
|
local theGroup = dcsCommon.createEmptyAircraftGroupData (name)
|
|
local theAUnit = dcsCommon.createAircraftUnitData(name .. "-civA", theTypeString, false)
|
|
theAUnit.payload.fuel = 100000
|
|
dcsCommon.addUnitToGroupData(theAUnit, theGroup)
|
|
|
|
local fromWP = dcsCommon.createTakeOffFromParkingRoutePointData(fromAirfield)
|
|
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
|
|
-- 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
|
|
end
|
|
|
|
dcsCommon.moveGroupDataTo(theGroup,
|
|
fromWP.x,
|
|
fromWP.y)
|
|
dcsCommon.addRoutePointForGroupData(theGroup, fromWP)
|
|
dcsCommon.addRoutePointForGroupData(theGroup, overheadWP)
|
|
dcsCommon.addRoutePointForGroupData(theGroup, toWP)
|
|
|
|
-- spawn
|
|
local groupCat = Group.Category.AIRPLANE
|
|
local theSpawnedGroup = coalition.addGroup(82, groupCat, theGroup) -- 82 is UN peacekeepers
|
|
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
|
|
|
|
local name = fAB:getName() .. "-" .. sAB:getName().. "/" .. 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:getName() .. "> to <" .. sAB:getName() .. ">", 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
|
|
end
|
|
|
|
--
|
|
-- U P D A T E L O O P
|
|
--
|
|
|
|
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
|
|
--Group.destroy(group) -- may break
|
|
end
|
|
end
|
|
|
|
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)
|
|
end
|
|
|
|
|
|
-- now, run through all existing flights and update their
|
|
-- idle times. also count how many planes there are
|
|
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 aPlane:isExist() 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
|
|
civAir.idlePlanes[name] = -1000
|
|
speed = 0
|
|
end
|
|
else
|
|
-- force removal
|
|
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
|
|
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 aFlight:isExist() then
|
|
-- destroy can only work if group isexist!
|
|
Group.destroy(aFlight) -- remember: flights are groups!
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
|
|
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
|
|
local value = cfxZones.getStringFromZoneProperty(aZone, "civAir", "")
|
|
local af = dcsCommon.getClosestAirbaseTo(aZone.point, 0) -- 0 = only airfields, not farp or ships
|
|
if af then
|
|
local afName = af:getName()
|
|
if value:lower() == "exclude" then
|
|
table.insert(civAir.excludeAirfields, afName)
|
|
else
|
|
table.insert(civAir.trafficCenters, afName)
|
|
end
|
|
end
|
|
|
|
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
|
|
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 < 1 then
|
|
trigger.action.outText("+++civTraffic: 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()
|
|
--trigger.action.outText("+++civTraffic: adding " .. afName, 30)
|
|
table.insert(civAir.trafficCenters, afName)
|
|
end
|
|
end
|
|
|
|
civAir.listTrafficCenters()
|
|
|
|
-- air-start half population if allowed
|
|
if civAir.initialAirSpawns then
|
|
civAir.airStartPopulation()
|
|
end
|
|
|
|
-- start the update loop
|
|
civAir.update()
|
|
|
|
-- say hi!
|
|
trigger.action.outText("cf/x civTraffic 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
|
|
source to target method
|
|
--]]-- |