mirror of
https://github.com/FlightControl-Master/MOOSE_MISSIONS.git
synced 2026-08-27 15:37:03 +00:00
1749 lines
62 KiB
Lua
1749 lines
62 KiB
Lua
--[[
|
|
Copyright (c) 2016 Snafu, Stonehouse, Rivvern, Chameleon Silk, lukrop.
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute copies of the Software,
|
|
and to permit persons to whom the Software is furnished to do so, subject to
|
|
the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software and the Software shall not be
|
|
included in whole or part in any sort of paid for software or paid for downloadable
|
|
content (DLC) without the express permission of the copyright holders.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
]]
|
|
|
|
--[[--
|
|
## Overview
|
|
Autonomous GCI and CAP script for DCS: World.
|
|
The script provides an autonomous model of combat air patrols and ground controlled
|
|
interceptors for use with DCS World by mission builders.
|
|
|
|
After minimal setup the script will automatically spawn CAP and GCI flights for two
|
|
sides and give them patrol and intercept tasks as well as returning them to base when
|
|
threats cease to be detected.
|
|
|
|
Originally created by Snafu, enhanced and further modified by Stonehouse,
|
|
Rivvern, Chameleon Silk.
|
|
|
|
Rewritten by lukrop.
|
|
|
|
## Links
|
|
|
|
Github repository: <https://github.com/lukrop/GCICAP>
|
|
|
|
@script GCICAP
|
|
@author Snafu
|
|
@author Stonehouse
|
|
@author Rivvern
|
|
@author Chameleon Silk
|
|
@author lukrop
|
|
@copyright 2016 Snafu, Stonehouse, Rivvern, Chameleon Silk, lukrop.
|
|
@license Modified MIT. See LICENSE file.
|
|
]]
|
|
|
|
gcicap = {}
|
|
gcicap.red = {}
|
|
gcicap.red.gci = {}
|
|
gcicap.red.cap = {}
|
|
gcicap.blue = {}
|
|
gcicap.blue.gci = {}
|
|
gcicap.blue.cap = {}
|
|
gcicap.cap = {}
|
|
gcicap.gci = {}
|
|
|
|
--- Sets how verbose the log output will be.
|
|
-- Possible values are "none", "info", "warning" and "error".
|
|
-- I recommend "error" for production.
|
|
gcicap.log_level = "error"
|
|
|
|
--- Interval, in seconds, of main function.
|
|
-- Default 30 seconds.
|
|
gcicap.interval = 30
|
|
|
|
--- Interval, in seconds, GCI flights get vectors on targets.
|
|
-- AI GCI flights don't use their radar, to be as stealth as
|
|
-- possible, relying on those vectors.
|
|
-- Default 15 seconds.
|
|
gcicap.vector_interval = 15
|
|
|
|
--- How far does a target have to move before an intercept is revectored
|
|
gcicap.revector_threshold = 15000
|
|
|
|
--- Initial spawn delay between CAPs
|
|
-- Default 30 seconds.
|
|
gcicap.initial_spawn_delay = 30
|
|
|
|
--- Enable/disable borders for the red side.
|
|
-- CAP units only engage if enemy units intrude their airspace
|
|
gcicap.red.borders_enabled = true
|
|
|
|
--- Enable/disable borders for the blue side.
|
|
-- CAP units only engage if enemy units intrude their airspace
|
|
gcicap.blue.borders_enabled = true
|
|
|
|
--- CAP minimum altitudes in meters.
|
|
-- Default 4500
|
|
gcicap.cap.min_alt = 6100
|
|
|
|
--- CAP maximum altitudes in meters.
|
|
-- Default 7500
|
|
gcicap.cap.max_alt = 10500
|
|
|
|
--- Speed for CAP flights on their CAP route.
|
|
-- speed is in m/s. Default 220.
|
|
gcicap.cap.speed = 220
|
|
|
|
--- Speed for GCI flights on intercept
|
|
-- speed is in m/s. Default 300.
|
|
gcicap.gci.speed = 300
|
|
|
|
--- Maximum engage distance for CAP flights as long as they are on patrol.
|
|
-- this might be overruled by an intercept vector given from
|
|
-- ground control (EWR). Default 15000.
|
|
gcicap.cap.max_engage_distance = 15000
|
|
|
|
--- Minimum red CAP VUL time in minutes.
|
|
-- Minimum time the red CAP flight will orbit on station.
|
|
gcicap.red.cap.vul_time_min = 25
|
|
|
|
--- Maximum red CAP VUL time in minutes.
|
|
-- Maximum time the red CAP flight will orbit on station.
|
|
gcicap.red.cap.vul_time_max = 40
|
|
|
|
--- Minimum blue CAP VUL time in minutes.
|
|
gcicap.blue.cap.vul_time_min = 25
|
|
|
|
--- Maximum blue CAP VUL time in minutes.
|
|
gcicap.blue.cap.vul_time_max = 30
|
|
|
|
--- Use race-track orbit for CAP flights
|
|
-- If true CAPs will use a race-track pattern for orbit
|
|
-- between two points in the CAP zone.
|
|
gcicap.cap.race_track_orbit = false
|
|
|
|
--[[ INOP at the time
|
|
--- Minimum leg length for red CAP orbits in meters.
|
|
gcicap.red.cap.leg_min = 10000
|
|
|
|
--- Maximum leg length for red CAP orbits in meters.
|
|
gcicap.red.cap.leg_min = 20000
|
|
|
|
--- Minimum leg length for blue CAP orbits in meters.
|
|
gcicap.blue.cap.leg_min = 10000
|
|
|
|
--- Maximum leg length for blue CAP orbits in meters.
|
|
gcicap.blue.cap.leg_min = 20000
|
|
]]--
|
|
|
|
--- Enable/disable red CAP flights airborne start.
|
|
-- set to true for CAP flight to start airborne at script initialisation
|
|
-- (mission start), false for taking off from the airfield.
|
|
-- Default true.
|
|
gcicap.red.cap.start_airborne = false
|
|
|
|
--- Enable/disable blue CAP flights airborne start.
|
|
gcicap.blue.cap.start_airborne = false
|
|
|
|
--- Amount of red CAP zones.
|
|
-- placed with triggerzones in the ME.
|
|
gcicap.red.cap.zones_count = 4
|
|
|
|
--- Amount of blue CAP zones.
|
|
gcicap.blue.cap.zones_count = 4
|
|
|
|
--- Amount of red CAP groups concurrently in the air.
|
|
gcicap.red.cap.groups_count = 3
|
|
|
|
--- Amount of blue CAP groups concurrently in the air.
|
|
gcicap.blue.cap.groups_count = 2
|
|
|
|
--- Group size of red CAP flights.
|
|
-- Can be "2", "4" or "randomized"
|
|
--
|
|
-- If "2" it consists of 2 planes, if "4" it consists of 4 planes
|
|
-- if "randomized", the CAP groups consist of either 2 or 4 planes
|
|
gcicap.red.cap.group_size = "randomized"
|
|
|
|
--- Group size of blue CAP flights.
|
|
-- See @{gcicap.red.cap.group_size}
|
|
gcicap.blue.cap.group_size = "4"
|
|
|
|
--- Maximum amount of concurrent red intercepts.
|
|
gcicap.red.gci.groups_count = 3
|
|
|
|
--- Maximum amount of concurrent blue intercepts.
|
|
gcicap.blue.gci.groups_count = 2
|
|
|
|
--- Group size of red GCI flights.
|
|
-- Can be "2", "4" or "dynamic"
|
|
--
|
|
-- If "2" it consists of 2 planes, if "4" it consists of 4 planes
|
|
-- if "dynamic", the GCI groups consist of as much aircrafts
|
|
-- as the intruder group.
|
|
gcicap.red.gci.group_size = "dynamic"
|
|
|
|
--- Group size of blue GCI flights.
|
|
-- See @{gcicap.red.gci.group_size}
|
|
gcicap.blue.gci.group_size = "dynamic"
|
|
|
|
--- Enable/disable GCI messages for red
|
|
gcicap.red.gci.messages = false
|
|
|
|
--- Enable/disable GCI messages for blue
|
|
gcicap.blue.gci.messages = false
|
|
|
|
--- How long a GCI message will be shown in seconds.
|
|
gcicap.gci.message_time = 5
|
|
|
|
--- Display GCI messages with metric measurment for red.
|
|
-- If false the imperial system is used.
|
|
gcicap.red.gci.messages_metric = true
|
|
|
|
--- Display GCI messages with metric measurment for blue.
|
|
-- If false the imperial system is used.
|
|
gcicap.blue.gci.messages_metric = false
|
|
|
|
--- Names of red groups which will receive GCI messages.
|
|
-- Leave blank for all groups of coalition
|
|
-- @usage gcicap.red.gci.messages_to = { "my group 1", "GCI Flight" }
|
|
gcicap.red.gci.messages_to = {}
|
|
|
|
--- Names of blue groups which will receive GCI messages.
|
|
-- See @{gcicap.red.gci.messages_to} for format.
|
|
gcicap.blue.gci.messages_to = {}
|
|
|
|
--- How red CAP flights are spawned.
|
|
-- can be "parking", "takeoff" or "air" and defines the way the fighters spawn
|
|
-- takeoff is NOT RECOMMENDED currently since their occur timing issues with tasking
|
|
-- if a flight is queued for takeoff and not already in the game world while getting tasked
|
|
--
|
|
-- Default 'parking'
|
|
gcicap.red.cap.spawn_mode = "parking"
|
|
|
|
--- How red GCI flights are spawned.
|
|
-- @see gcicap.red.cap.spawn_mode
|
|
gcicap.red.gci.spawn_mode = "parking"
|
|
|
|
--- How blue CAP flights are spawned.
|
|
-- @see gcicap.red.cap.spawn_mode
|
|
gcicap.blue.cap.spawn_mode = "parking"
|
|
|
|
--- How blue GCI flights are spawned.
|
|
-- @see gcicap.red.cap.spawn_mode
|
|
gcicap.blue.gci.spawn_mode = "parking"
|
|
|
|
--- Hide or reveal blue air units in the mission.
|
|
gcicap.blue.hide_groups = false
|
|
|
|
--- Hide or reveal red air units in the mission.
|
|
gcicap.red.hide_groups = false
|
|
|
|
--- Enable/disable red CAP flights.
|
|
gcicap.red.cap.enabled = true
|
|
|
|
--- Enable/disable blue CAP flights.
|
|
gcicap.blue.cap.enabled = true
|
|
|
|
--- Enable/disable red GCI flights.
|
|
gcicap.red.gci.enabled = true
|
|
|
|
--- Enable/disable blue GCI flights.
|
|
gcicap.blue.gci.enabled = true
|
|
|
|
--- Enabel/disable resource limitation for red.
|
|
-- If set to true limits the amount of groups a side can spawn.
|
|
gcicap.red.limit_resources = false
|
|
|
|
--- Enabel/disable resource limitation for blue.
|
|
-- @see gcicap.red.limit_resources
|
|
gcicap.blue.limit_resources = false
|
|
|
|
--- Amount of groups(!) red has at it's disposal.
|
|
-- In other words how many Groups of airplanes
|
|
-- this side can spawn.
|
|
gcicap.red.supply = 24
|
|
|
|
--- Amount of groups(!) red has at it's disposal.
|
|
-- @see gcicap.red.supply
|
|
gcicap.blue.supply = 24
|
|
|
|
--- Name of the trigger zone which defines red CAP zones.
|
|
-- This will be postfixed with the number of
|
|
-- the zone. e.g. "redCAPzone3" or "blueCAPzone1".
|
|
--
|
|
-- Default: 'redCAPzone'.
|
|
gcicap.red.cap.zone_name = 'redCAPzone'
|
|
|
|
--- Name of the trigger zone which defines blue CAP zones.
|
|
-- Default: 'blueCAPzone'.
|
|
-- @see gcicap.red.cap.zone_name
|
|
gcicap.blue.cap.zone_name = 'blueCAPzone'
|
|
|
|
--- Name of group which waypoints define the red border.
|
|
-- Default: 'redborder'.
|
|
gcicap.red.border_group = 'redborder'
|
|
|
|
--- Name of group which waypoints define the blue border.
|
|
-- Default: 'blueborder'.
|
|
gcicap.blue.border_group = 'blueborder'
|
|
|
|
--- GCI template unit's names prefix.
|
|
gcicap.gci.template_prefix = '__GCI__'
|
|
|
|
--- CAP template unit's names prefix.
|
|
gcicap.cap.template_prefix = '__CAP__'
|
|
|
|
--- Count of template units.
|
|
-- Remember that this means you need that many
|
|
-- template units for each type. E.g. if the template_count is 2 you
|
|
-- would need two GCI and two CAP template units for each side.
|
|
gcicap.template_count = 4
|
|
|
|
--- Wether red will also acquire targets by AWACS aircraft.
|
|
-- This is is currently broken since isTargetDetected doesn't
|
|
-- seem to work with AWACS airplanes. Needs a workaround.
|
|
--
|
|
-- Default false.
|
|
gcicap.red.awacs = false
|
|
|
|
--- Wether blue will also acquire targets by AWACS aircraft.
|
|
-- @see gcicap.red.awacs
|
|
gcicap.blue.awacs = true
|
|
|
|
--- Garbage collector move timeout
|
|
-- If a unit (aircraft) is on the ground and didn't move
|
|
-- since this timeout, in seconds, it will be removed.
|
|
-- This applies only to aircraft spawned by GCICAP.
|
|
gcicap.move_timeout = 300
|
|
|
|
-- shortcut to the bullseye
|
|
gcicap.red.bullseye = coalition.getMainRefPoint(coalition.side.RED)
|
|
gcicap.blue.bullseye = coalition.getMainRefPoint(coalition.side.BLUE)
|
|
|
|
gcicap.sides = { "red", "blue" }
|
|
gcicap.tasks = { "cap", "gci" }
|
|
|
|
gcicap.log = mist.Logger:new("GCICAP", gcicap.log_level)
|
|
|
|
do
|
|
--- Flight class.
|
|
-- @type gcicap.Flight
|
|
gcicap.Flight = {}
|
|
|
|
local function getFlightIndex(group)
|
|
if type(group) ~= "string" then
|
|
if group:getName() then
|
|
group = group:getName()
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
for i, side in pairs(gcicap.sides) do
|
|
for j, task in pairs(gcicap.tasks) do
|
|
for n = 1, #gcicap[side][task].flights do
|
|
if gcicap[side][task].flights[n].group_name == group then
|
|
return {side = side, task = task, index = n}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Returns the flight for the given group.
|
|
-- @tparam string|Group group this can be a Group object
|
|
-- or the group name.
|
|
-- @treturn gcicap.Flight the flight for the given group.
|
|
function gcicap.Flight.getFlight(group)
|
|
f = getFlightIndex(group)
|
|
if f then
|
|
return gcicap[f.side][f.task].flights[f.index]
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
--- Creates a new flight.
|
|
-- @tparam Group group group of the flight.
|
|
-- @tparam Airbase airbase homplate of the new flight.
|
|
-- @tparam string task task of the new flight. Can be "cap" or "gci".
|
|
-- @param param task parameter. This can be a zone table if it's a
|
|
-- CAP flight or it could be a target unit if it's a GCI flight.
|
|
function gcicap.Flight:new(group, airbase, task, param)
|
|
if group:isExist() then
|
|
local side = gcicap.coalitionToSide(group:getCoalition())
|
|
local f = {}
|
|
f.side = side
|
|
f.give_up = false
|
|
f.group = group
|
|
f.group_name = group:getName()
|
|
f.airbase = airbase
|
|
f.task = task
|
|
-- is the flight RTB?
|
|
f.rtb = false
|
|
f.in_zone = false
|
|
|
|
if task == "cap" then
|
|
f.zone = param
|
|
f.zone_name = param.name
|
|
f.intercepting = false
|
|
f.vul_time = math.random(gcicap[side].cap.vul_time_min,
|
|
gcicap[side].cap.vul_time_max)
|
|
else -- task should be "gci"
|
|
f.target = param
|
|
f.target_group = param.group
|
|
f.intercepting = true
|
|
f.intercept_point = { x = 0, y = 0, z = 0 }
|
|
end
|
|
|
|
-- get current timestamp
|
|
local timestamp = timer.getAbsTime()
|
|
f.units_moved = {}
|
|
-- set timestamp for each unit
|
|
-- this is later used for garbage collection checks
|
|
for u, unit in pairs(group:getUnits()) do
|
|
f.units_moved[u] = {}
|
|
f.units_moved[u].unit = unit
|
|
f.units_moved[u].last_moved = timestamp
|
|
f.units_moved[u].spawned_at = timestamp
|
|
end
|
|
|
|
setmetatable(f, self)
|
|
self.__index = self
|
|
|
|
table.insert(gcicap[side][task].flights, f)
|
|
gcicap.log:info("Registered flight: $1", f.group_name)
|
|
|
|
return f
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
--- Removes the flight
|
|
-- @tparam gcicap.Flight self flight object
|
|
function gcicap.Flight:remove()
|
|
if self.zone then
|
|
-- if we didn't already leave the zone do it now.
|
|
self:leaveCAPZone()
|
|
end
|
|
local f = getFlightIndex(self.group_name)
|
|
local r = table.remove(gcicap[f.side][f.task].flights, f.index)
|
|
if r then
|
|
gcicap.log:info("Removing flight $1 with index $2", r.group_name, f.index)
|
|
end
|
|
end
|
|
|
|
--- Decreases active flights counter in this flights zone.
|
|
-- Actually just decreases the active flights
|
|
-- counter of a zone. Does NOT task the flight itself.
|
|
function gcicap.Flight:leaveCAPZone()
|
|
if self.in_zone then
|
|
local zone = self.zone
|
|
if zone.patrol_count <= 1 then
|
|
zone.patrol_count = 0
|
|
else
|
|
zone.patrol_count = zone.patrol_count - 1
|
|
end
|
|
self.in_zone = false
|
|
|
|
-- get current time
|
|
local time_now = timer.getAbsTime()
|
|
-- get time on station by substracting vul start time from current time
|
|
-- and convert it to minutes
|
|
local time_on_station = 0
|
|
if self.vul_start then
|
|
time_on_station = (time_now - self.vul_start) / 60
|
|
end
|
|
local vul_diff = self.vul_time - time_on_station
|
|
-- set new vul time only if more than 5 minutes
|
|
if vul_diff > 5 then
|
|
self.vul_time = vul_diff
|
|
else
|
|
self.vul_time = 0
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Increases active flights counter in this flights zone.
|
|
-- Actually just increases the active flights
|
|
-- counter of a zone. Does NOT task the flight itself.
|
|
function gcicap.Flight:enterCAPZone()
|
|
if not self.in_zone then
|
|
self.intercepting = false
|
|
self.in_zone = true
|
|
local zone = self.zone
|
|
zone.patrol_count = zone.patrol_count + 1
|
|
end
|
|
end
|
|
|
|
--- Tasks the flight to search and engage the target.
|
|
-- @tparam Unit intruder target unit.
|
|
-- @tparam[opt] boolean cold whether the flight should not destroy
|
|
-- the target and just follow it. Default false.
|
|
function gcicap.Flight:vectorToTarget(intruder, cold)
|
|
local target = nil
|
|
if intruder.group then
|
|
target = gcicap.getFirstActiveUnit(intruder.group)
|
|
end
|
|
if target == nil or intruder.group == nil then return end
|
|
|
|
-- check if interceptor even still exists
|
|
if self.group:isExist() then
|
|
if target:isExist() and target:inAir() and self.give_up ~= true then
|
|
local target_pos = target:getPoint()
|
|
local ctl = self.group:getController()
|
|
|
|
local gci_task = {
|
|
id = 'Mission',
|
|
params = {
|
|
route = {
|
|
points = {
|
|
[1] = {
|
|
alt = target_pos.y,
|
|
x = target_pos.x,
|
|
y = target_pos.z,
|
|
speed = gcicap.gci.speed,
|
|
action = "Turning Point",
|
|
type = "Turning Point",
|
|
task = {
|
|
id = "ComboTask",
|
|
params = {
|
|
tasks = {
|
|
[1] = {
|
|
number = 1,
|
|
key = "CAP",
|
|
id = "EngageTargets",
|
|
enabled = true,
|
|
auto = true,
|
|
params = {
|
|
targetTypes = { [1] = "Air" },
|
|
priority = 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[2] = {
|
|
alt = target_pos.y,
|
|
x = target_pos.x,
|
|
y = target_pos.z,
|
|
speed = gcicap.gci.speed,
|
|
action = "Turning Point",
|
|
type = "Turning Point",
|
|
task = {
|
|
-- i don't really like this WrappedAction but it's needed in
|
|
-- the case the CGI completes this waypoint because of lack/loss
|
|
-- of target
|
|
id = 'WrappedAction',
|
|
params = {
|
|
action = {
|
|
id = 'Script',
|
|
params = {
|
|
command = "local group = ...\
|
|
local flight = gcicap.Flight.getFlight(group)\
|
|
if flight then\
|
|
flight.give_up = true\
|
|
if flight.zone then\
|
|
if flight.intercepting then\
|
|
flight:taskWithCAP()\
|
|
end\
|
|
else\
|
|
if not flight.target then\
|
|
flight:taskWithRTB()\
|
|
end\
|
|
end\
|
|
else\
|
|
gcicap.log:error('Could not find flight')\
|
|
end"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
-- checkout of the patrol zone
|
|
if self.zone and not self.intercepting then
|
|
self:leaveCAPZone()
|
|
end
|
|
|
|
intruder.intercepted = true
|
|
-- only set/reset the task if the target has moved significantly since last GCI update
|
|
if mist.utils.get3DDist( target_pos, self.intercept_point ) > gcicap.revector_threshold then
|
|
-- if there's still an EWR detecting or we are responding to the initial call
|
|
-- then set the target position. do not allow revectoring if no EWR is detecting us now
|
|
if (Unit.isExist(intruder.detected_by) and intruder.detected_by:isActive()) then
|
|
self.give_up = false
|
|
self.intercept_point = mist.utils.deepCopy(target_pos)
|
|
ctl:setTask(gci_task)
|
|
gcicap.log:info("Vectoring $1 to $2 ($3)", self.group:getName(),
|
|
intruder.group:getName(), target:getName())
|
|
|
|
else
|
|
gcicap.log:info("Cannot revector $1 to $2 because no longer detecting",self.group:getName(),intruder.group:getName())
|
|
end
|
|
end
|
|
self.intercepting = true
|
|
|
|
-- taskEngageGroup provides omniscient knowledge of where the group to be attacked is, which sucks
|
|
if not cold then
|
|
--gcicap.taskEngageGroup(self.group, intruder.group)
|
|
gcicap.taskEngage(self.group, 15000)
|
|
end
|
|
|
|
-- reschedule function until either the interceptor or the intruder is dead
|
|
mist.scheduleFunction(gcicap.Flight.vectorToTarget, {self, intruder, cold},
|
|
timer.getTime() + gcicap.vector_interval)
|
|
|
|
else -- the target is dead or we had to give up, resume CAP or RTB
|
|
if self.zone then
|
|
-- send CAP back to work only if still intercepting
|
|
if self.intercepting then
|
|
self:taskWithCAP()
|
|
end
|
|
else
|
|
self.intercepting = false
|
|
-- send GCI back to homeplate
|
|
self:taskWithRTB()
|
|
end
|
|
end
|
|
else
|
|
-- our interceptor group is dead let's see if the
|
|
-- intruder is still there and set him to not beeing intercepted anymore
|
|
if target:isExist() then
|
|
intruder.intercepted = false
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Tasks flight with combat air patrol.
|
|
-- Creates waypoints inside it's assigned zone and tasks
|
|
-- the flight with patroling along the route.
|
|
-- @tparam[opt] boolean cold If set to true the flight won't
|
|
-- engage any enemy unit's it detects by itself. Default false.
|
|
function gcicap.Flight:taskWithCAP(cold)
|
|
-- only task with CAP if ther is still vul time left
|
|
if self.vul_time == 0 then
|
|
-- send flight RTB if no vul time left.
|
|
gcicap.log:info("No vul time left for $1", self.group_name)
|
|
self:taskWithRTB()
|
|
else
|
|
local group = self.group
|
|
local ctl = group:getController()
|
|
local side = gcicap.coalitionToSide(group:getCoalition())
|
|
local start_pos = gcicap.getFirstActiveUnit(group):getPoint()
|
|
local leg_dist = math.random(gcicap[side].cap.leg_min, gcicap[side].cap.leg_max)
|
|
local cap_route = gcicap.buildCAPRoute(start_pos, self.zone.name, self.vul_time, leg_dist)
|
|
local cap_task = {
|
|
id = 'Mission',
|
|
params = {
|
|
route = cap_route
|
|
}
|
|
}
|
|
|
|
self.intercepting = false
|
|
self.intercept_point = { x = 0, y = 0, z = 0 }
|
|
ctl:setTask(cap_task)
|
|
self:enterCAPZone()
|
|
ctl:setOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_SEARCH_IF_REQUIRED)
|
|
|
|
if not cold then
|
|
gcicap.taskEngage(group)
|
|
end
|
|
gcicap.log:info("Tasking $1 with CAP in zone $2", group:getName(), self.zone.name)
|
|
end
|
|
end
|
|
|
|
--- Tasks the flight to return to it's homeplate.
|
|
-- @tparam[opt] Airbase airbase optionally use this as homeplate/airbase
|
|
-- to return to.
|
|
-- @tparam[opt] boolean cold If set to true the flight won't
|
|
-- engage any targets it detects on the way back to base.
|
|
-- Default false.
|
|
function gcicap.Flight:taskWithRTB(airbase, cold)
|
|
if not airbase then
|
|
airbase = self.airbase
|
|
end
|
|
|
|
if self.zone then
|
|
self:leaveCAPZone()
|
|
local side = self.side
|
|
-- let's try to spawn a new CAP flight as soon as the current one is tasked with RTB.
|
|
-- never spawn more than 2 x the groups_count, to prevent spam in case something ever goes wrong.
|
|
if (not gcicap[side].limit_resources or
|
|
(gcicap[side].limit_resources and gcicap[side].supply > 0))
|
|
and #gcicap[side].cap.flights < gcicap[side].cap.groups_count * 2 then
|
|
gcicap.spawnCAP(side, self.zone, gcicap[side].cap.spawn_mode)
|
|
end
|
|
end
|
|
self.rtb = true
|
|
local group = self.group
|
|
local ctl = group:getController()
|
|
local af_pos = mist.utils.makeVec2(airbase:getPoint())
|
|
local af_id = airbase:getID()
|
|
local rtb_task = {
|
|
id = 'Mission',
|
|
params = {
|
|
route = {
|
|
points = {
|
|
[1] = {
|
|
alt = gcicap.cap.min_alt,
|
|
alt_type = "BARO",
|
|
speed = gcicap.cap.speed,
|
|
x = af_pos.x,
|
|
y = af_pos.y,
|
|
aerodromeId = af_id,
|
|
type = "Land",
|
|
action = "Landing",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ctl:setTask(rtb_task)
|
|
|
|
if not cold then
|
|
-- only engage if enemy is inside of 10km of the leg
|
|
gcicap.taskEngage(group, 10000)
|
|
end
|
|
|
|
gcicap.log:info("Tasking $1 with RTB to $2", group:getName(), airbase:getName())
|
|
end
|
|
|
|
--- Functions
|
|
-- @section gcicap
|
|
|
|
--- Clean up inactive/stuck flights.
|
|
local function garbageCollector(side)
|
|
local timestamp = timer.getAbsTime()
|
|
for t, task in pairs(gcicap.tasks) do
|
|
for f, flight in pairs(gcicap[side][task].flights) do
|
|
if flight.group then
|
|
if flight.group:isExist() then
|
|
for u = 1, #flight.units_moved do
|
|
local unit = flight.units_moved[u].unit
|
|
-- check if unit exists
|
|
if unit then
|
|
if unit:isExist() then
|
|
-- if unit is in air we won't do anything
|
|
if not unit:inAir() then
|
|
-- check if unit is moving
|
|
local mag = mist.vec.mag(unit:getVelocity())
|
|
if mag == 0 then
|
|
-- get the last time the unit moved
|
|
local last_moved = flight.units_moved[u].last_moved
|
|
if timestamp - last_moved > gcicap.move_timeout then
|
|
gcicap.log:info("Cleaning up $1", flight.group:getName())
|
|
flight.group:destroy()
|
|
flight:remove()
|
|
end
|
|
else
|
|
flight.units_moved[u].last_moved = timestamp
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
flight:remove()
|
|
end
|
|
else
|
|
flight:remove()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function checkForTemplateUnits(side)
|
|
if gcicap[side].gci.enabled then
|
|
for i = 1, gcicap.template_count do
|
|
local unit = gcicap.gci.template_prefix..side..i
|
|
if not Unit.getByName(unit) then
|
|
gcicap.log:alert("GCI template unit missing: $1", unit)
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
if gcicap[side].cap.enabled then
|
|
for i = 1, gcicap.template_count do
|
|
local unit = gcicap.cap.template_prefix..side..i
|
|
if not Unit.getByName(unit) then
|
|
gcicap.log:alert("CAP template unit missing: $1", unit)
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
if gcicap[side].borders_enabled then
|
|
if not Group.getByName(gcicap[side].border_group) then
|
|
gcicap.log:alert("Border group is missing: $1", gcicap[side].border_group)
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function checkForTriggerZones(side)
|
|
for i = 1, gcicap[side].cap.zones_count do
|
|
local zone_name = gcicap[side].cap.zone_name..i
|
|
if not trigger.misc.getZone(zone_name) then
|
|
gcicap.log:alert("CAP trigger zone is missing: $1", zone_name)
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function manageCAP(side)
|
|
local patroled_zones = 0
|
|
|
|
for i = 1, #gcicap[side].cap.zones do
|
|
local zone = gcicap[side].cap.zones[i]
|
|
gcicap.log:info("Zone $1 has $2 patrols", zone.name, zone.patrol_count)
|
|
|
|
-- see if we can send a new CAP into the zone
|
|
if zone.patrol_count <= 0 then
|
|
-- first check if we already hit the maximum amounts of routine CAP groups
|
|
if #gcicap[side].cap.flights < gcicap[side].cap.groups_count then
|
|
-- check if we limit resources and if we have enough supplies
|
|
-- if we don't limit resource or have enough supplies we spawn
|
|
if not gcicap[side].limit_resources or
|
|
(gcicap[side].limit_resources and gcicap[side].supply > 0) then
|
|
-- finally spawn it
|
|
gcicap.spawnCAP(side, gcicap[side].cap.zones[i], gcicap[side].cap.spawn_mode)
|
|
end
|
|
end
|
|
else
|
|
patroled_zones = patroled_zones + 1
|
|
end
|
|
end
|
|
-- if all zones are patroled and we still have cap groups left
|
|
-- send them to a random zone
|
|
if #gcicap[side].cap.flights < gcicap[side].cap.groups_count then
|
|
if not gcicap[side].limit_resources or
|
|
(gcicap[side].limit_resources and gcicap[side].supply > 0) then
|
|
local random_zone = math.random(1, #gcicap[side].cap.zones)
|
|
gcicap.spawnCAP(side, gcicap[side].cap.zones[random_zone], gcicap[side].cap.spawn_mode)
|
|
end
|
|
end
|
|
gcicap.log:info("$1 patrols in $2/$3 zones with $4 flights",
|
|
side, patroled_zones, gcicap[side].cap.zones_count, #gcicap[side].cap.flights)
|
|
end
|
|
|
|
local function handleIntrusion(side)
|
|
for i = 1, #gcicap[side].intruders do
|
|
local intruder = gcicap[side].intruders[i]
|
|
if intruder.group then
|
|
if intruder.group:isExist() then
|
|
-- check if we need to do something about him
|
|
if not intruder.intercepted then
|
|
-- first check if we have something to work with
|
|
if #gcicap[side].cap.flights > 0
|
|
or #gcicap[side].gci.flights > 0
|
|
or #gcicap[side].gci.flights < gcicap[side].gci.groups_count then
|
|
-- get closest flight to intruder if there is any
|
|
local closest = nil
|
|
local intruder_unit = gcicap.getFirstActiveUnit(intruder.group)
|
|
local closest_flights = gcicap.getClosestFlightsToUnit(side, intruder_unit)
|
|
-- we found close flights
|
|
local flight_avail = false
|
|
if closest_flights then
|
|
for j = 1, #closest_flights do
|
|
closest = closest_flights[j]
|
|
--fligh_avail = (not closest.flight.rtb) and (not closest.flight.intercepting)
|
|
flight_avail = (not closest.flight.intercepting)
|
|
if flight_avail then
|
|
gcicap.log:info("Found flight $1 which is avaliable for tasking.",
|
|
closest.flight.group:getName())
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if flight_avail then
|
|
-- check if we have a airfield which is closer to the unit than the closest flight
|
|
-- but add some distance to the airfield since it takes time for a potential spawned
|
|
-- flight to take-off
|
|
local closest_af, af_distance = gcicap.getClosestAirfieldToUnit(side, intruder_unit)
|
|
af_distance = af_distance + 15000 -- add 15km
|
|
if closest.distance < af_distance or af_distance == -1 then
|
|
-- task flight with intercept
|
|
closest.flight.give_up = false
|
|
closest.flight:vectorToTarget(intruder)
|
|
return
|
|
end
|
|
if (not gcicap[side].limit_resources
|
|
or (gcicap[side].limit_resources and gcicap[side].supply > 0))
|
|
and #gcicap[side].gci.flights < gcicap[side].gci.groups_count
|
|
and gcicap[side].gci.enabled then
|
|
-- spawn CGI
|
|
gcicap.log:info("Airfield closer to intruder than flight or no flight available. Spawning GCI")
|
|
local gci = gcicap.spawnGCI(side, intruder)
|
|
end
|
|
else
|
|
if (not gcicap[side].limit_resources
|
|
or (gcicap[side].limit_resources and gcicap[side].supply > 0))
|
|
and #gcicap[side].gci.flights < gcicap[side].gci.groups_count
|
|
and gcicap[side].gci.enabled then
|
|
-- spawn CGI
|
|
gcicap.log:info("No CAP flights or already airborne GCI. Spawning GCI")
|
|
local gci = gcicap.spawnGCI(side, intruder)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- the intruder group doesn't exist (anymore) remove it
|
|
table.remove(gcicap[side].intruders, i)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- returns airfields of given side which are marked with
|
|
-- triggerzones (triggerzone name is exactly the same as airfield name).
|
|
local function getAirfields(side)
|
|
local coal_airfields = coalition.getAirbases(gcicap.sideToCoalition(side))
|
|
local gcicap_airfields = {}
|
|
|
|
-- loop over all coalition airfields
|
|
for i = 1, #coal_airfields do
|
|
-- get name of airfield
|
|
local af_name = coal_airfields[i]:getName()
|
|
if not string.match(af_name, "FARP") then
|
|
-- check if a triggerzone exists with that exact name
|
|
if mist.DBs.zonesByName[af_name] then
|
|
-- add it to our airfield list for gcicap
|
|
gcicap_airfields[#gcicap_airfields + 1] = coal_airfields[i]
|
|
end
|
|
end
|
|
end
|
|
|
|
if #gcicap_airfields == 0 then
|
|
gcicap.log:warn("No airbase for $1 found", side)
|
|
end
|
|
return gcicap_airfields
|
|
end
|
|
|
|
-- returns all currently active aircraft of the given side
|
|
-- parameter side has to be "red" or "blue"
|
|
local function getAllActiveAircrafts(side)
|
|
local filter = { "[" .. side .. "][plane]", "[" .. side .. "][helicopter]"}
|
|
local all_aircraft = mist.makeUnitTable(filter)
|
|
local active_aircraft = {}
|
|
|
|
for i = 1, #all_aircraft do
|
|
local ac = Unit.getByName(all_aircraft[i])
|
|
if ac ~= nil then
|
|
if Unit.isActive(ac) then
|
|
table.insert(active_aircraft, ac)
|
|
end
|
|
end
|
|
end
|
|
if #active_aircraft == 0 then
|
|
gcicap.log:warn("No active aircraft for $1 found", side)
|
|
end
|
|
return active_aircraft
|
|
end
|
|
|
|
-- returns all currently active EWR and AWACS units of the given side
|
|
-- parameter side has to be "red" or "blue"
|
|
local function getAllActiveEWR(side)
|
|
local filter = { "[" .. side .. "][plane]", "[" .. side .. "][vehicle]", "[" .. side .. "][ship]"}
|
|
local all_vecs = mist.makeUnitTable(filter)
|
|
local active_ewr = {}
|
|
|
|
for i = 1, #all_vecs do
|
|
local vec = Unit.getByName(all_vecs[i])
|
|
if vec ~= nil then
|
|
if Unit.isActive(vec) then
|
|
local vec_type = Unit.getTypeName(vec)
|
|
if vec_type == "55G6 EWR"
|
|
or vec_type == "1L13 EWR"
|
|
or vec_type == "Hawk sr"
|
|
or vec_type == "Patriot str" then
|
|
table.insert(active_ewr, { unit = vec, is_awacs = false} )
|
|
end
|
|
-- ED has a bug; the E-2D vehicle has type E-2C
|
|
if (vec_type == "A-50" and gcicap[side].awacs)
|
|
or (vec_type == "E-2C" and gcicap[side].awacs)
|
|
or (vec_type == "E-3A" and gcicap[side].awacs) then
|
|
table.insert(active_ewr, { unit = vec, is_awacs = true} )
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if #active_ewr == 0 then
|
|
gcicap.log:warn("No active EWR for $1 found", side)
|
|
end
|
|
return active_ewr
|
|
end
|
|
|
|
local function checkForAirspaceIntrusion(side)
|
|
-- init some local vars
|
|
local border = gcicap[side].border
|
|
local active_ewr = gcicap[side].active_ewr
|
|
local intruder_count = 0
|
|
local intruder_side = ""
|
|
local toremove = {}
|
|
if side == "red" then
|
|
-- set the side of the intruder
|
|
intruder_side = "blue"
|
|
elseif side == "blue" then
|
|
intruder_side = "red"
|
|
end
|
|
local active_ac = gcicap[intruder_side].active_aircraft
|
|
|
|
-- only do something if we have active ewr and active aircraft
|
|
if #active_ac > 0 and #active_ewr > 0 then
|
|
-- loop over all aircraft
|
|
for i = 1, #active_ac do
|
|
local ac = active_ac[i]
|
|
local ac_detected = false
|
|
local ac_intruded = false
|
|
local ac_pos = {}
|
|
local ac_group = nil
|
|
local intruder_num = 0
|
|
local ewr = nil
|
|
if ac ~= nil then
|
|
ac_group = ac:getGroup()
|
|
if ac_group:isExist() then
|
|
ac_pos = ac:getPoint()
|
|
|
|
-- now loop over all ewr units
|
|
for n = 1, #active_ewr do
|
|
local ewr_controller = nil
|
|
if active_ewr[n].is_awacs then
|
|
ewr_controller = active_ewr[n].unit:getController()
|
|
else
|
|
ewr_controller = active_ewr[n].unit:getGroup():getController()
|
|
end
|
|
-- and check if the EWR detected the aircraft
|
|
if ewr_controller:isTargetDetected(ac, RADAR) then
|
|
ewr = active_ewr[n].unit
|
|
ac_detected = true
|
|
-- stop once it was detected by one EWR
|
|
break
|
|
end
|
|
end
|
|
|
|
if ac_detected then
|
|
-- do we check borders?
|
|
if gcicap[side].borders_enabled then
|
|
ac_intruded = mist.pointInPolygon(ac_pos, border)
|
|
else
|
|
-- if not the aircarft is always intruding
|
|
ac_intruded = true
|
|
end
|
|
|
|
if ac_intruded then
|
|
local in_list = false
|
|
-- check if we already know about the intruder
|
|
for j = 1, #gcicap[side].intruders do
|
|
if gcicap[side].intruders[j].name == ac_group:getName() then
|
|
in_list = true
|
|
intruder_num = j
|
|
break
|
|
end
|
|
end
|
|
if not in_list then
|
|
intruder_count = intruder_count + 1
|
|
|
|
gcicap.log:info("$1 ($2) intruded airspace of $3 detected by $4 ($5)",
|
|
ac_group:getName(), ac:getName(), side,
|
|
ewr:getGroup():getName(), ewr:getName())
|
|
|
|
intruder = {
|
|
name = ac_group:getName(),
|
|
--unit = ac,
|
|
group = ac_group,
|
|
detected_by = ewr,
|
|
--groupID = ac_group:getID(),
|
|
--unitID = ac:getID(),
|
|
--unitType = ac:getTypeName(),
|
|
size = ac_group:getSize(),
|
|
intercepted = false,
|
|
}
|
|
table.insert(gcicap[side].intruders, intruder)
|
|
intruder_num = #gcicap[side].intruders
|
|
end
|
|
|
|
-- send message to all units of coalition or some specified groups
|
|
-- that we have a intruder
|
|
if gcicap[side].gci.messages then
|
|
local par = {
|
|
units = { ac:getName() },
|
|
ref = gcicap[side].bullseye,
|
|
alt = ac_pos.y,
|
|
}
|
|
-- do we want to display in metric units?
|
|
if gcicap[side].gci.messages_metric then
|
|
par.metric = true
|
|
end
|
|
|
|
local msg_for = {}
|
|
-- if groups are specified find their units names and add them to the list
|
|
if #gcicap[side].gci.messages_to > 0 then
|
|
msg_for.units = {}
|
|
for g, group_name in pairs(gcicap[side].gci.messages_to) do
|
|
group = Group.getByName(group_name)
|
|
if group ~= nil then
|
|
for u, unit in pairs(group:getUnits()) do
|
|
table.insert(msg_for.units, unit:getName())
|
|
end
|
|
end
|
|
end
|
|
else
|
|
msg_for.coa = { side }
|
|
end
|
|
-- get the bearing, range and altitude from bullseye to intruder
|
|
local bra = mist.getBRString(par)
|
|
local bra_string = "Airpsace intrusion! BRA from bullseye "..bra
|
|
local msg = {
|
|
text = bra_string,
|
|
displayTime = gcicap.gci.message_time,
|
|
msgFor = msg_for,
|
|
name = "gcicap.gci.msg"..intruder_num,
|
|
}
|
|
-- finally send the message
|
|
mist.message.add(msg)
|
|
end
|
|
end -- if ac_intruded
|
|
else
|
|
-- the ac is _not_ intruding so we should remove it from the intruders list
|
|
|
|
local in_list = false
|
|
local intruder_num = 0
|
|
-- check if we already know about the intruder
|
|
for j = 1, #gcicap[side].intruders do
|
|
if gcicap[side].intruders[j].name == ac_group:getName() then
|
|
in_list = true
|
|
intruder_num = j
|
|
break
|
|
end
|
|
end
|
|
if in_list then toremove[#toremove + 1] = intruder_num end
|
|
end -- if ac_detected
|
|
end -- if ac_group is existing
|
|
end -- if ac ~= nil
|
|
end -- for #active_ac
|
|
end -- if active_ac > 0 and active_ewr > 0
|
|
|
|
-- we need to remove intruders from outside the loop
|
|
if #toremove > 0 then
|
|
for i = 1,#toremove do
|
|
intruder_count = intruder_count - 1
|
|
gcicap.log:info("Aircraft "..gcicap[side].intruders[i].name.." no longer intruding")
|
|
table.remove(gcicap[side].intruders,i)
|
|
end
|
|
end
|
|
if intruder_count > 0 then
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
--return gcicap[side].intruders
|
|
end
|
|
|
|
-- returns a random airfield for the given side
|
|
local function getRandomAirfield(side)
|
|
local rand = math.random(1, #gcicap[side].airfields)
|
|
return gcicap[side].airfields[rand]
|
|
end
|
|
|
|
local function buildFirstWp(airbase, spawn_mode)
|
|
local airbase_pos = airbase:getPoint()
|
|
local airbase_id = airbase:getID()
|
|
local wp = mist.fixedWing.buildWP(airbase_pos)
|
|
|
|
if spawn_mode == "parking" then -- start from parking area
|
|
wp.airdromeId = airbase_id
|
|
wp.type = "TakeOffParking"
|
|
wp.action = "From Parking Area"
|
|
elseif spawn_mode == "takeoff" then -- or start from runway
|
|
wp.airdromeId = airbase_id
|
|
wp.type = "TakeOff"
|
|
wp.action = "From Runway"
|
|
elseif spawn_mode == "air" then
|
|
-- randomize spawn position a little bit in case of air start
|
|
wp.x = wp.x + (50 * math.sin(math.random(10)))
|
|
wp.y = wp.y + (50 * math.sin(math.random(10)))
|
|
end
|
|
|
|
return wp
|
|
end
|
|
|
|
--- Converts coaltion number to side string.
|
|
-- 0 = "neutral", 1 = "red", 2 = "blue"
|
|
-- @tparam number coal coaltion number.
|
|
-- @treturn string side
|
|
function gcicap.coalitionToSide(coal)
|
|
if coal == coalition.side.NEUTRAL then return "neutral"
|
|
elseif coal == coalition.side.RED then return "red"
|
|
elseif coal == coalition.side.BLUE then return "blue"
|
|
end
|
|
end
|
|
|
|
--- Converts side string to coaltion number.
|
|
-- 0 = "neutral", 1 = "red", 2 = "blue"
|
|
-- @tparam string side side string.
|
|
-- @treturn number coalition number.
|
|
-- @see coalitionToSide
|
|
function gcicap.sideToCoalition(side)
|
|
if side == "neutral" then return coalition.side.NEUTRAL
|
|
elseif side == "red" then return coalition.side.RED
|
|
elseif side == "blue" then return coalition.side.BLUE
|
|
end
|
|
end
|
|
|
|
--- Returns first active unit of a group.
|
|
-- @tparam Group group group whose first active
|
|
-- unit to return.
|
|
-- @treturn Unit first active unit of group.
|
|
function gcicap.getFirstActiveUnit(group)
|
|
if group ~= nil then
|
|
-- engrish mast0r isExistsingsed
|
|
if not group:isExist() then return nil end
|
|
local units = group:getUnits()
|
|
for i = 1, group:getSize() do
|
|
if units[i] then
|
|
return units[i]
|
|
end
|
|
end
|
|
return nil
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
--- Returns the closest airfield to unit.
|
|
-- Returned airfield is controlled by given side. This function
|
|
-- also returns the distance to the unit.
|
|
-- @tparam string side side string, either "red" or "blue".
|
|
-- The airfield returned has to be controlled by this side.
|
|
-- @tparam Unit unit unit to use as reference.
|
|
-- @treturn table @{closestAirfieldReturn}
|
|
function gcicap.getClosestAirfieldToUnit(side, unit)
|
|
if not unit then
|
|
gcicap.log:error("Couldn't find unit.")
|
|
return
|
|
end
|
|
local airfields = gcicap[side].airfields
|
|
|
|
if #airfields == 0 then
|
|
gcicap.log:warn("There are no airfields of side $1", side)
|
|
return nil
|
|
end
|
|
|
|
local unit_pos = mist.utils.makeVec2(unit:getPoint())
|
|
local min_distance = -1
|
|
local closest_af = nil
|
|
|
|
for i = 1, #airfields do
|
|
local af = airfields[i]
|
|
local af_pos = mist.utils.makeVec2(af:getPoint())
|
|
local distance = mist.utils.get2DDist(unit_pos, af_pos)
|
|
|
|
if distance < min_distance or min_distance == -1 then
|
|
min_distance = distance
|
|
closest_af = af
|
|
end
|
|
end
|
|
|
|
--- Table returned by getClosestAirfieldToUnit.
|
|
-- @table closestAirfieldReturn
|
|
-- @tfield Airbase airfield the Airbase object
|
|
-- @tfield number distance the distance in meters
|
|
-- to the unit.
|
|
--return {airfield = closest_af, distance = min_distance}
|
|
return closest_af, min_distance
|
|
end
|
|
|
|
--- Returns the closest flights to the given unit.
|
|
-- Flights returned are of given side. This function also returns
|
|
-- their distance to the unit. The returned flights are sorted
|
|
-- by distance. First is the closest.
|
|
-- @tparam string side side whose flights to search.
|
|
-- @tparam Unit unit unit object used as reference.
|
|
-- @treturn table Array sorted by distance
|
|
-- containing @{closestFlightsReturn} tables.
|
|
function gcicap.getClosestFlightsToUnit(side, unit)
|
|
if not unit then
|
|
gcicap.log:error("Couldn't find unit.")
|
|
return
|
|
end
|
|
local closest_flights = {}
|
|
if #gcicap[side].cap.flights == 0 and #gcicap[side].gci.flights == 0 then
|
|
gcicap.log:info("No CAP or GCI flights of side $1 active", side)
|
|
return nil
|
|
else
|
|
local unit_pos = mist.utils.makeVec2(unit:getPoint())
|
|
local min_distance = -1
|
|
for t, task in pairs(gcicap.tasks) do
|
|
local flights = gcicap[side][task].flights
|
|
for i = 1, #flights do
|
|
if flights[i].group then
|
|
local u = gcicap.getFirstActiveUnit(flights[i].group)
|
|
if u then
|
|
local u_pos = mist.utils.makeVec2(u:getPoint())
|
|
local distance = mist.utils.get2DDist(unit_pos, u_pos)
|
|
table.insert(closest_flights, {flight = flights[i], distance = distance })
|
|
else
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- sort closest flights
|
|
table.sort(closest_flights, function(a,b)
|
|
if a.distance < b.distance then
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end)
|
|
|
|
--- Table returned by getClosestFlightsToUnit.
|
|
-- @table closestFlightsReturn
|
|
-- @tfield gcicap.Flight flight object
|
|
-- @tfield number distance distance in meters from
|
|
-- the unit.
|
|
return closest_flights
|
|
end
|
|
end
|
|
|
|
--- Returns a table containting a CAP route.
|
|
-- Route originating from given airbase, waypoints
|
|
-- are placed randomly inside given zone. Optionally
|
|
-- you can specify the amount of waypoints inside the zone.
|
|
-- @tparam string zone trigger zone name
|
|
-- @tparam number vul_time time on station
|
|
-- @tparam number leg_distance leg distance for race-track pattern orbit.
|
|
function gcicap.buildCAPRoute(start_pos, zone, vul_time, leg_distance)
|
|
local points = {}
|
|
-- make altitude consistent for the whole route.
|
|
local alt = math.random(gcicap.cap.min_alt, gcicap.cap.max_alt)
|
|
|
|
local start_vul_script = "local group = ...\
|
|
local flight = gcicap.Flight.getFlight(group)\
|
|
if flight then\
|
|
gcicap.log:info('$1 starting vul time $2 at $3',\
|
|
flight.group_name, flight.vul_time, flight.zone.name)\
|
|
flight.vul_start = timer.getAbsTime()\
|
|
else\
|
|
gcicap.log:error('Could not find flight')\
|
|
end"
|
|
|
|
local end_vul_script = "local group = ...\
|
|
local flight = gcicap.Flight.getFlight(group)\
|
|
if flight then\
|
|
gcicap.log:info('$1 vul time over at $2',\
|
|
flight.group_name, flight.zone.name)\
|
|
flight:taskWithRTB()\
|
|
else\
|
|
gcicap.log:error('Could not find flight')\
|
|
end"
|
|
|
|
-- build orbit start waypoint
|
|
local orbit_start_point = mist.getRandomPointInZone(zone)
|
|
-- add a bogus waypoint so the start vul time script block
|
|
-- isn't executed instantly after tasking
|
|
points[1] = mist.fixedWing.buildWP(start_pos)
|
|
points[2] = mist.fixedWing.buildWP(orbit_start_point)
|
|
points[2].task = {}
|
|
points[2].task.id = 'ComboTask'
|
|
points[2].task.params = {}
|
|
points[2].task.params.tasks = {}
|
|
points[2].task.params.tasks[1] = {
|
|
number = 1,
|
|
auto = false,
|
|
id = 'WrappedAction',
|
|
enabled = true,
|
|
params = {
|
|
action = {
|
|
id = 'Script',
|
|
params = {
|
|
command = start_vul_script
|
|
}
|
|
}
|
|
}
|
|
}
|
|
points[2].task.params.tasks[2] = {
|
|
number = 2,
|
|
auto = false,
|
|
id = 'ControlledTask',
|
|
enabled = true,
|
|
params = {
|
|
task = {
|
|
id = 'Orbit',
|
|
params = {
|
|
altitude = alt,
|
|
pattern = 'Race-Track',
|
|
speed = gcicap.cap.speed
|
|
}
|
|
},
|
|
stopCondition = {
|
|
duration = vul_time * 60
|
|
}
|
|
}
|
|
}
|
|
|
|
-- if we don't use the race-track pattern we'll add the vul end time
|
|
-- waypoint right where the start waypoint is and use the 'Circle' pattern.
|
|
local orbit_end_point
|
|
if not gcicap.cap.race_track_orbit then
|
|
points[2].task.params.tasks[2].params.task.params.pattern = 'Circle'
|
|
orbit_end_point = start_pos
|
|
else
|
|
-- build second waypoint (leg end waypoint)
|
|
--local orbit_end_point = mist.getRandPointInCircle(orbit_start_point, leg_distance, leg_distance)
|
|
orbit_end_point = mist.getRandomPointInZone(zone)
|
|
end
|
|
|
|
points[3] = mist.fixedWing.buildWP(orbit_end_point)
|
|
points[3].task = {
|
|
id = 'WrappedAction',
|
|
params = {
|
|
action = {
|
|
id = 'Script',
|
|
params = {
|
|
command = end_vul_script
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for i = 1, 3 do
|
|
points[i].speed = gcicap.cap.speed
|
|
points[i].alt = alt
|
|
end
|
|
|
|
-- local ground_level = land.getHeight(point)
|
|
-- -- avoid crashing into hills
|
|
-- if (alt - 100) < ground_level then
|
|
-- alt = alt + ground_level
|
|
-- end
|
|
|
|
gcicap.log:info("Built CAP route with $1 min vul time at $2 meters in $3", vul_time, alt, zone)
|
|
|
|
local route = {}
|
|
route.points = points
|
|
return route
|
|
end
|
|
|
|
--- Tasks group to automatically engage any spotted targets.
|
|
-- @tparam Group group group to task.
|
|
-- @tparam[opt] number max_dist maximum engagment distance.
|
|
-- Targets further out (from the route) won't be engaged.
|
|
function gcicap.taskEngage(group, max_dist)
|
|
if not max_dist then
|
|
max_dist = gcicap.cap.max_engage_distance
|
|
end
|
|
local ctl = group:getController()
|
|
local engage = {
|
|
id = 'EngageTargets',
|
|
params = {
|
|
maxDist = max_dist,
|
|
maxDistEnabled = true,
|
|
targetTypes = { [1] = "Air" },
|
|
priority = 0
|
|
}
|
|
}
|
|
ctl:pushTask(engage)
|
|
end
|
|
|
|
--- Tasks group to engage a group.
|
|
-- @tparam Group group group to task.
|
|
-- @tparam Group target group that should be engaged by
|
|
-- given group.
|
|
function gcicap.taskEngageGroup(group, target)
|
|
local ctl = group:getController()
|
|
local engage_group = {
|
|
id = 'EngageGroup',
|
|
params = {
|
|
groupId = target:getID(),
|
|
directionEnabled = false,
|
|
priority = 0,
|
|
altittudeEnabled = false,
|
|
}
|
|
}
|
|
ctl:pushTask(engage_group)
|
|
end
|
|
|
|
--- Spawns a fighter group.
|
|
-- @tparam string side side of the newly created group.
|
|
-- Can be "red" or "blue".
|
|
-- @tparam string name new group name.
|
|
-- @tparam number size count of aircraft in the new group.
|
|
-- @tparam Airbase airbase home plate of the new group.
|
|
-- @tparam string spawn_mode How the new group will be spawned.
|
|
-- Can be 'parking' or 'air'. 'parking' will spawn them at the ramp
|
|
-- wit engines turned off. 'air' will spawn them in the air already
|
|
-- flying.
|
|
-- @tparam string task Task of the new group. Can either be 'cap',
|
|
-- for combat air patrol, or 'gci', for ground controlled intercept.
|
|
-- @tparam[opt] string zone zone name in which to spawn the unit. This only is
|
|
-- taken into account if spawn_mode is "in-zone".
|
|
-- @tparam[opt] boolean cold if set to true the newly group won't engage
|
|
-- any enemys until tasked otherwise. Default false.
|
|
-- @treturn Group|nil newly spawned group or nil on failure.
|
|
function gcicap.spawnFighterGroup(side, name, size, airbase, spawn_mode, task, zone, cold)
|
|
local template_unit_name = gcicap[task].template_prefix..side..math.random(1, gcicap.template_count)
|
|
local template_unit = Unit.getByName(template_unit_name)
|
|
if not template_unit then
|
|
gcicap.log:error("Can't find template unit $1. This should never happen.\
|
|
Somehow the template unit got deleted.", template_unit_name)
|
|
return nil
|
|
end
|
|
local template_group = mist.getGroupData(template_unit:getGroup():getName())
|
|
local template_unit_data = template_group.units[1]
|
|
local airbase_pos = airbase:getPoint()
|
|
local group_data = {}
|
|
local unit_data = {}
|
|
local onboard_num = template_unit_data.onboard_num - 1
|
|
local route = {}
|
|
|
|
local rand_point = {}
|
|
if spawn_mode == "in-zone" then
|
|
rand_point = mist.getRandomPointInZone(zone)
|
|
end
|
|
|
|
for i = 1, size do
|
|
unit_data[i] = {}
|
|
unit_data[i].type = template_unit_data.type
|
|
unit_data[i].name = name.." Pilot "..i
|
|
if spawn_mode == "in-zone" then
|
|
unit_data[i].x = rand_point.x + (50 * math.sin(math.random(10)))
|
|
unit_data[i].y = rand_point.y + (50 * math.sin(math.random(10)))
|
|
else
|
|
unit_data[i].x = airbase_pos.x + (50 * math.sin(math.random(10)))
|
|
unit_data[i].y = airbase_pos.z + (50 * math.sin(math.random(10)))
|
|
end
|
|
unit_data[i].alt = gcicap[side].cap.min_alt
|
|
unit_data[i].onboard_num = onboard_num + i
|
|
unit_data[i].groupName = name
|
|
unit_data[i].payload = template_unit_data.payload
|
|
unit_data[i].skill = template_unit_data.skill
|
|
unit_data[i].livery_id = template_unit_data.livery_id
|
|
if side == 'blue' then
|
|
unit_data[i].callsign = {}
|
|
unit_data[i].callsign[1] = 4 -- Colt
|
|
unit_data[i].callsign[2] = gcicap[side].cap.flight_num
|
|
unit_data[i].callsign[3] = i
|
|
else
|
|
unit_data[i].callsign = 600 + gcicap[side].cap.flight_num + i
|
|
end
|
|
end
|
|
|
|
group_data.units = unit_data
|
|
group_data.groupName = name
|
|
group_data.hidden = gcicap[side].hide_groups
|
|
--group_data.country = template_group.country
|
|
group_data.country = template_unit:getCountry()
|
|
group_data.category = template_group.category
|
|
group_data.task = "CAP"
|
|
|
|
route.points = {}
|
|
if spawn_mode == "in-zone" then
|
|
route.points[1] = mist.fixedWing.buildWP(rand_point)
|
|
route.points[1].alt = gcicap[side].cap.min_alt
|
|
route.points[1].speed = gcicap[side].cap.speed
|
|
else
|
|
route.points[1] = buildFirstWp(airbase, spawn_mode)
|
|
end
|
|
group_data.route = route
|
|
|
|
if mist.groupTableCheck(group_data) then
|
|
local spawn_pos = airbase:getName()
|
|
if spawn_mode == "in-zone" then
|
|
spawn_pos = zone
|
|
end
|
|
gcicap.log:info("Spawning fighter group $1 at $2", name, spawn_pos)
|
|
mist.dynAdd(group_data)
|
|
else
|
|
gcicap.log:error("Couldn't spawn group with following groupTable: $1", group_data)
|
|
end
|
|
|
|
return Group.getByName(name)
|
|
end
|
|
|
|
--- Spawns a CAP flight.
|
|
-- @tparam string side side for the new CAP.
|
|
-- @tparam string zone CAP zone (trigger zone) name.
|
|
-- @tparam string spawn_mode how the new CAP will be spawned.
|
|
-- Can be 'parking' or 'air'.
|
|
function gcicap.spawnCAP(side, zone, spawn_mode)
|
|
-- increase flight number
|
|
gcicap[side].cap.flight_num = gcicap[side].cap.flight_num + 1
|
|
-- select random airbase (for now) TODO: choose closest airfield
|
|
local airbase = getRandomAirfield(side)
|
|
local group_name = "CAP "..side.." "..gcicap[side].cap.flight_num
|
|
-- define size of the flight
|
|
local size = gcicap[side].cap.group_size
|
|
if size == "randomized" then
|
|
size = math.random(1,2)*2
|
|
else
|
|
size = tonumber(size)
|
|
end
|
|
-- actually spawn something
|
|
local group = gcicap.spawnFighterGroup(side, group_name, size, airbase, spawn_mode, "cap", zone.name)
|
|
--local ctl = group:getController()
|
|
--ctl:setOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_ATTACK_ONLY)
|
|
gcicap[side].supply = gcicap[side].supply - 1
|
|
-- keep track of the flight
|
|
local flight = gcicap.Flight:new(group, airbase, "cap", zone)
|
|
-- task the group, for some odd reason we have to wait until we use setTask
|
|
-- on a freshly spawned group.
|
|
mist.scheduleFunction(gcicap.Flight.taskWithCAP, {flight}, timer.getTime() + 5)
|
|
return group
|
|
end
|
|
|
|
--- Spawns a GCI flight.
|
|
-- @tparam string side side for the new GCI.
|
|
-- @tparam Unit intruder unit to intercept.
|
|
-- @tparam Airbase airbase airbase at which to spawn the GCI flight.
|
|
function gcicap.spawnGCI(side, intruder, airbase)
|
|
-- increase flight number
|
|
gcicap[side].gci.flight_num = gcicap[side].gci.flight_num + 1
|
|
-- select closest airfield to unit
|
|
local intruder_unit = gcicap.getFirstActiveUnit(intruder.group)
|
|
local closest_af = gcicap.getClosestAirfieldToUnit(side, intruder_unit)
|
|
if closest_af then
|
|
airbase = closest_af
|
|
else
|
|
gcicap.log:warn("Couldn't find close airfield for GCI. Choosing one at random.")
|
|
airbase = getRandomAirfield(side)
|
|
end
|
|
local tgt_units = intruder.group:getUnits()
|
|
local group_name = "GCI "..side.." "..gcicap[side].gci.flight_num
|
|
-- define size of the flight
|
|
local size = gcicap[side].gci.group_size
|
|
if size == "randomized" then
|
|
size = math.random(1,2)*2
|
|
elseif size == "dynamic" then
|
|
size = #tgt_units
|
|
else
|
|
size = tonumber(size)
|
|
end
|
|
-- actually spawn something
|
|
local group = gcicap.spawnFighterGroup(side, group_name, size, airbase, gcicap[side].gci.spawn_mode, "gci")
|
|
local ctl = group:getController()
|
|
-- make the GCI units only use their radar for attacking
|
|
ctl:setOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_ATTACK_ONLY)
|
|
gcicap[side].supply = gcicap[side].supply - 1
|
|
-- keep track of the flight
|
|
local flight = gcicap.Flight:new(group, airbase, "gci", intruder)
|
|
-- vector the interceptor group on the target the first time.
|
|
mist.scheduleFunction(gcicap.Flight.vectorToTarget, {flight, intruder}, timer.getTime() + 5)
|
|
return group
|
|
end
|
|
|
|
--- Initialization function
|
|
-- Checks if all template units are present. Creates
|
|
-- border polygons if borders enabled.
|
|
-- @todo complete documentation.
|
|
function gcicap.init()
|
|
for i, side in pairs(gcicap.sides) do
|
|
if not (checkForTemplateUnits(side) and checkForTriggerZones(side)) then
|
|
return false
|
|
end
|
|
if gcicap[side].borders_enabled then
|
|
gcicap[side].border = mist.getGroupPoints(gcicap[side].border_group)
|
|
end
|
|
gcicap[side].intruders = {}
|
|
gcicap[side].cap.zones = {}
|
|
gcicap[side].cap.flights = {}
|
|
gcicap[side].gci.flights = {}
|
|
gcicap[side].cap.flight_num = 0
|
|
gcicap[side].gci.flight_num = 0
|
|
gcicap[side].airfields = getAirfields(side)
|
|
|
|
if gcicap[side].cap.enabled then
|
|
-- loop through all zones
|
|
for i = 1, gcicap[side].cap.zones_count do
|
|
local zone_name = gcicap[side].cap.zone_name..i
|
|
local point = trigger.misc.getZone(zone_name).point
|
|
local size = trigger.misc.getZone(zone_name).radius
|
|
|
|
-- create zone table
|
|
gcicap[side].cap.zones[i] = {
|
|
name = zone_name,
|
|
pos = point,
|
|
radius = size,
|
|
patrol_count = 0,
|
|
}
|
|
end
|
|
|
|
for i = 1, gcicap[side].cap.groups_count do
|
|
local spawn_mode = "parking"
|
|
if gcicap[side].cap.start_airborne then
|
|
spawn_mode = "in-zone"
|
|
end
|
|
-- try to fill all zones
|
|
local zone = gcicap[side].cap.zones[i]
|
|
-- if we have more flights than zones we select one random zone
|
|
if zone == nil then
|
|
zone = gcicap[side].cap.zones[math.random(1, gcicap[side].cap.zones_count)]
|
|
end
|
|
-- actually spawn the group
|
|
--local grp = gcicap.spawnCAP(side, zone, spawn_mode)
|
|
-- delay the spawn by gcicap interval seconds after one another
|
|
local spawn_delay = (i - 1) * gcicap.initial_spawn_delay
|
|
mist.scheduleFunction(gcicap.spawnCAP, {side, zone, spawn_mode}, timer.getTime() + spawn_delay)
|
|
end
|
|
end
|
|
end
|
|
-- add event handler managing despawns
|
|
return true
|
|
end
|
|
|
|
--- Main function.
|
|
-- Run approx. every @{gcicap.interval} sconds. A random amount
|
|
-- of 0 to 2 seconds is added for declustering.
|
|
-- @todo do the "declustering" at a different level. Probably
|
|
-- more efficient.
|
|
function gcicap.main()
|
|
for i, side in pairs(gcicap.sides) do
|
|
-- update list of occupied airfields
|
|
gcicap[side].airfields = getAirfields(side)
|
|
-- update list of all aircraft
|
|
gcicap[side].active_aircraft = getAllActiveAircrafts(side)
|
|
-- update list of all EWR
|
|
gcicap[side].active_ewr = getAllActiveEWR(side)
|
|
end
|
|
|
|
-- check for airspace intrusions after updating all the lists
|
|
for i, side in pairs(gcicap.sides) do
|
|
if gcicap[side].cap.enabled then
|
|
manageCAP(side)
|
|
end
|
|
checkForAirspaceIntrusion(side)
|
|
handleIntrusion(side)
|
|
garbageCollector(side)
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
if gcicap.init() then
|
|
local start_delay = gcicap.initial_spawn_delay * math.max(gcicap.red.cap.groups_count, gcicap.blue.cap.groups_count)
|
|
mist.scheduleFunction(gcicap.main, {}, timer.getTime() + start_delay, gcicap.interval)
|
|
end
|
|
|
|
-- vim: sw=2:ts=2
|