Merge pull request #1183 from dcs-liberation/helipads

[WIP] Add possibility to add helipads to FOB control points
This commit is contained in:
C. Perreau 2021-09-08 22:54:05 +02:00 committed by GitHub
commit 82f5287282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 281 additions and 28 deletions

View File

@ -7,7 +7,8 @@ Saves from 4.x are not compatible with 5.0.
* **[Campaign]** Weather! Theaters now experience weather that is more realistic for the region and its current season. For example, Persian Gulf will have very hot, sunny summers and Marianas will experience lots of rain during fall. These changes affect pressure, temperature, clouds and precipitation. Additionally, temperature will drop during the night, by an amount that is somewhat realistic for the region. * **[Campaign]** Weather! Theaters now experience weather that is more realistic for the region and its current season. For example, Persian Gulf will have very hot, sunny summers and Marianas will experience lots of rain during fall. These changes affect pressure, temperature, clouds and precipitation. Additionally, temperature will drop during the night, by an amount that is somewhat realistic for the region.
* **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated. * **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated.
* **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts. * **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts.
* **[Campaign]** Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/discussions/1550 for details. * **[Campaign]** FOBs control point can have FARP/helipad slot and host helicopters. To enable this feature on a FOB, add "Invisible FARP" statics objects near the FOB location in the campaign definition file.
* **[Campaign]** Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/issues/1145 for status.
* **[Campaign]** Aircraft now belong to squadrons rather than bases to support squadron location transfers. * **[Campaign]** Aircraft now belong to squadrons rather than bases to support squadron location transfers.
* **[Campaign]** Skipped turns are no longer counted as defeats on front lines. * **[Campaign]** Skipped turns are no longer counted as defeats on front lines.
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions. * **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.

View File

@ -45,7 +45,7 @@ class MizCampaignLoader:
SHIPPING_LANE_UNIT_TYPE = HandyWind.id SHIPPING_LANE_UNIT_TYPE = HandyWind.id
FOB_UNIT_TYPE = Unarmed.SKP_11.id FOB_UNIT_TYPE = Unarmed.SKP_11.id
FARP_HELIPAD = "SINGLE_HELIPAD" FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD"]
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
@ -212,7 +212,7 @@ class MizCampaignLoader:
@property @property
def helipads(self) -> Iterator[StaticGroup]: def helipads(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group: for group in self.blue.static_group:
if group.units[0].type == self.FARP_HELIPAD: if group.units[0].type in self.FARP_HELIPADS_TYPE:
yield group yield group
@property @property

View File

@ -184,6 +184,10 @@ class AircraftType(UnitType[Type[FlyingType]]):
def flyable(self) -> bool: def flyable(self) -> bool:
return self.dcs_unit_type.flyable return self.dcs_unit_type.flyable
@property
def helicopter(self) -> bool:
return self.dcs_unit_type.helicopter
@cached_property @cached_property
def max_speed(self) -> Speed: def max_speed(self) -> Speed:
return kph(self.dcs_unit_type.max_speed) return kph(self.dcs_unit_type.max_speed)

View File

@ -8,6 +8,8 @@ from datetime import date, datetime, timedelta
from enum import Enum from enum import Enum
from typing import Any, List, Type, Union, cast, TYPE_CHECKING from typing import Any, List, Type, Union, cast, TYPE_CHECKING
from dcs.countries import Switzerland, UnitedNationsPeacekeepers, USAFAggressors
from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
@ -195,6 +197,17 @@ class Game:
) )
) )
@property
def neutral_country(self) -> Type[Country]:
"""Return the best fitting country that can be used as neutral faction in the generated mission"""
countries_in_use = [self.red.country_name, self.blue.country_name]
if UnitedNationsPeacekeepers not in countries_in_use:
return UnitedNationsPeacekeepers
elif Switzerland.name not in countries_in_use:
return Switzerland
else:
return USAFAggressors
def _generate_events(self) -> None: def _generate_events(self) -> None:
for front_line in self.theater.conflicts(): for front_line in self.theater.conflicts():
self._generate_player_event( self._generate_player_event(

View File

@ -39,6 +39,7 @@ from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from gen.visualgen import VisualGenerator from gen.visualgen import VisualGenerator
from .. import db from .. import db
from ..theater import Airfield, FrontLine from ..theater import Airfield, FrontLine
from ..theater.bullseye import Bullseye
from ..unitmap import UnitMap from ..unitmap import UnitMap
if TYPE_CHECKING: if TYPE_CHECKING:
@ -105,6 +106,9 @@ class Operation:
cls.current_mission.coalition["red"] = Coalition( cls.current_mission.coalition["red"] = Coalition(
"red", bullseye=cls.game.red.bullseye.to_pydcs() "red", bullseye=cls.game.red.bullseye.to_pydcs()
) )
cls.current_mission.coalition["neutrals"] = Coalition(
"neutrals", bullseye=Bullseye(Point(0, 0)).to_pydcs()
)
p_country = cls.game.blue.country_name p_country = cls.game.blue.country_name
e_country = cls.game.red.country_name e_country = cls.game.red.country_name
@ -115,6 +119,16 @@ class Operation:
country_dict[db.country_id_from_name(e_country)]() country_dict[db.country_id_from_name(e_country)]()
) )
belligerents = [
db.country_id_from_name(p_country),
db.country_id_from_name(e_country),
]
for country in country_dict.keys():
if country not in belligerents:
cls.current_mission.coalition["neutrals"].add_country(
country_dict[country]()
)
@classmethod @classmethod
def inject_lua_trigger(cls, contents: str, comment: str) -> None: def inject_lua_trigger(cls, contents: str, comment: str) -> None:
trigger = TriggerStart(comment=comment) trigger = TriggerStart(comment=comment)
@ -365,6 +379,7 @@ class Operation:
cls.laser_code_registry, cls.laser_code_registry,
cls.unit_map, cls.unit_map,
air_support=cls.airsupportgen.air_support, air_support=cls.airsupportgen.air_support,
helipads=cls.groundobjectgen.helipads,
) )
cls.airgen.clear_parking_slots() cls.airgen.clear_parking_slots()

View File

@ -32,6 +32,7 @@ from dcs.ships import (
) )
from dcs.terrain.terrain import Airport, ParkingSlot from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unittype import FlyingType
from game import db from game import db
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
@ -410,6 +411,13 @@ class ControlPoint(MissionTarget, ABC):
return True return True
return False return False
@property
def has_helipads(self) -> bool:
"""
Returns true if cp has helipads
"""
return len(self.helipads) > 0
def can_recruit_ground_units(self, game: Game) -> bool: def can_recruit_ground_units(self, game: Game) -> bool:
"""Returns True if this control point is capable of recruiting ground units.""" """Returns True if this control point is capable of recruiting ground units."""
if not self.can_deploy_ground_units: if not self.can_deploy_ground_units:
@ -828,6 +836,22 @@ class ControlPoint(MissionTarget, ABC):
"""Return the number of ammo depots, including dead ones""" """Return the number of ammo depots, including dead ones"""
return len(list(self.all_ammo_depots)) return len(list(self.all_ammo_depots))
@property
def active_fuel_depots_count(self) -> int:
"""Return the number of available fuel depots"""
return len(
[
obj
for obj in self.connected_objectives
if obj.category == "fuel" and not obj.is_dead
]
)
@property
def total_fuel_depots_count(self) -> int:
"""Return the number of fuel depots, including dead ones"""
return len([obj for obj in self.connected_objectives if obj.category == "fuel"])
@property @property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]: def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
return [] return []
@ -886,6 +910,11 @@ class Airfield(ControlPoint):
@property @property
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
"""
Return total aircraft parking slots available
Note : additional helipads shouldn't contribute to this score as it could allow airfield
to buy more planes than what they are able to host
"""
return len(self.airport.parking_slots) return len(self.airport.parking_slots)
@property @property
@ -1165,7 +1194,7 @@ class Fob(ControlPoint):
self.name = name self.name = name
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
return False return self.has_helipads
def active_runway( def active_runway(
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData] self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
@ -1189,10 +1218,10 @@ class Fob(ControlPoint):
@property @property
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
return 0 return len(self.helipads)
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
return False return aircraft.helicopter
@property @property
def heading(self) -> Heading: def heading(self) -> Heading:

View File

@ -111,6 +111,9 @@ VERSION = _build_version_string()
#: * DCS 2.7.4.9632 changed scenery target IDs. Any mission using map buildings as #: * DCS 2.7.4.9632 changed scenery target IDs. Any mission using map buildings as
#: strike targets must check and potentially recreate all those objectives. #: strike targets must check and potentially recreate all those objectives.
#: #:
#: Version 8.1
#: * You can now add "Invisible FARP" static to FOB to add helicopter slots
#:
#: Version 9.0 #: Version 9.0
#: * Campaign files now define the initial squadron layouts. See TODO. #: * Campaign files now define the initial squadron layouts. See TODO.
#: * CV and LHA control points now get their names from the group name in the campaign #: * CV and LHA control points now get their names from the group name in the campaign

View File

@ -229,6 +229,7 @@ class AircraftConflictGenerator:
laser_code_registry: LaserCodeRegistry, laser_code_registry: LaserCodeRegistry,
unit_map: UnitMap, unit_map: UnitMap,
air_support: AirSupport, air_support: AirSupport,
helipads: dict[ControlPoint, list[StaticGroup]],
) -> None: ) -> None:
self.m = mission self.m = mission
self.game = game self.game = game
@ -239,6 +240,7 @@ class AircraftConflictGenerator:
self.unit_map = unit_map self.unit_map = unit_map
self.flights: List[FlightData] = [] self.flights: List[FlightData] = []
self.air_support = air_support self.air_support = air_support
self.helipads = helipads
@cached_property @cached_property
def use_client(self) -> bool: def use_client(self) -> bool:
@ -534,6 +536,54 @@ class AircraftConflictGenerator:
group_size=count, group_size=count,
) )
def _generate_at_cp_helipad(
self,
name: str,
side: Country,
unit_type: Type[FlyingType],
count: int,
start_type: str,
cp: ControlPoint,
) -> FlyingGroup[Any]:
assert count > 0
logging.info(
"airgen at cp's helipads : {} for {} at {}".format(
unit_type, side.id, cp.name
)
)
try:
helipad = self.helipads[cp].pop()
except IndexError as ex:
raise RuntimeError(f"Not enough helipads available at {cp}") from ex
group = self._generate_at_group(
name=name,
side=side,
unit_type=unit_type,
count=count,
start_type=start_type,
at=helipad,
)
# Note : A bit dirty, need better support in pydcs
group.points[0].action = PointAction.FromGroundArea
group.points[0].type = "TakeOffGround"
group.units[0].heading = helipad.units[0].heading
if start_type != "Cold":
group.points[0].action = PointAction.FromGroundAreaHot
group.points[0].type = "TakeOffGroundHot"
for i in range(count - 1):
try:
helipad = self.helipads[cp].pop()
group.units[1 + i].position = Point(helipad.x, helipad.y)
group.units[1 + i].heading = helipad.units[0].heading
except IndexError as ex:
raise RuntimeError(f"Not enough helipads available at {cp}") from ex
return group
def _add_radio_waypoint( def _add_radio_waypoint(
self, self,
group: FlyingGroup[Any], group: FlyingGroup[Any],
@ -692,11 +742,13 @@ class AircraftConflictGenerator:
self, cp: ControlPoint, country: Country, flight: Flight self, cp: ControlPoint, country: Country, flight: Flight
) -> FlyingGroup[Any]: ) -> FlyingGroup[Any]:
name = namegen.next_aircraft_name(country, cp.id, flight) name = namegen.next_aircraft_name(country, cp.id, flight)
group: FlyingGroup[Any]
try: try:
if flight.start_type == "In Flight": if flight.start_type == "In Flight":
group = self._generate_inflight( group = self._generate_inflight(
name=name, side=country, flight=flight, origin=cp name=name, side=country, flight=flight, origin=cp
) )
return group
elif isinstance(cp, NavalControlPoint): elif isinstance(cp, NavalControlPoint):
group_name = cp.get_carrier_group_name() group_name = cp.get_carrier_group_name()
carrier_group = self.m.find_group(group_name) carrier_group = self.m.find_group(group_name)
@ -705,7 +757,7 @@ class AircraftConflictGenerator:
f"Carrier group {carrier_group} is a " f"Carrier group {carrier_group} is a "
"{carrier_group.__class__.__name__}, expected a ShipGroup" "{carrier_group.__class__.__name__}, expected a ShipGroup"
) )
group = self._generate_at_group( return self._generate_at_group(
name=name, name=name,
side=country, side=country,
unit_type=flight.unit_type.dcs_unit_type, unit_type=flight.unit_type.dcs_unit_type,
@ -714,11 +766,22 @@ class AircraftConflictGenerator:
at=carrier_group, at=carrier_group,
) )
else: else:
# If the flight is an helicopter flight, then prioritize dedicated helipads
if flight.unit_type.helicopter:
return self._generate_at_cp_helipad(
name=name,
side=country,
unit_type=flight.unit_type.dcs_unit_type,
count=flight.count,
start_type=flight.start_type,
cp=cp,
)
if not isinstance(cp, Airfield): if not isinstance(cp, Airfield):
raise RuntimeError( raise RuntimeError(
f"Attempted to spawn at airfield for non-airfield {cp}" f"Attempted to spawn at airfield for non-airfield {cp}"
) )
group = self._generate_at_airport( return self._generate_at_airport(
name=name, name=name,
side=country, side=country,
unit_type=flight.unit_type.dcs_unit_type, unit_type=flight.unit_type.dcs_unit_type,
@ -737,8 +800,7 @@ class AircraftConflictGenerator:
name=name, side=country, flight=flight, origin=cp name=name, side=country, flight=flight, origin=cp
) )
group.points[0].alt = 1500 group.points[0].alt = 1500
return group
return group
@staticmethod @staticmethod
def set_reduced_fuel( def set_reduced_fuel(

View File

@ -220,6 +220,8 @@ class FlightWaypoint:
PointAction.FromParkingArea: FlightWaypointType.TAKEOFF, PointAction.FromParkingArea: FlightWaypointType.TAKEOFF,
PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF, PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF,
PointAction.FromRunway: FlightWaypointType.TAKEOFF, PointAction.FromRunway: FlightWaypointType.TAKEOFF,
PointAction.FromGroundArea: FlightWaypointType.TAKEOFF,
PointAction.FromGroundAreaHot: FlightWaypointType.TAKEOFF,
}[point.action] }[point.action]
if waypoint.waypoint_type == FlightWaypointType.NAV: if waypoint.waypoint_type == FlightWaypointType.NAV:
waypoint.name = "NAV" waypoint.name = "NAV"

View File

@ -9,6 +9,7 @@ from __future__ import annotations
import logging import logging
import random import random
from collections import defaultdict
from typing import ( from typing import (
Dict, Dict,
Iterator, Iterator,
@ -35,7 +36,7 @@ from dcs.task import (
FireAtPoint, FireAtPoint,
) )
from dcs.triggers import TriggerStart, TriggerZone from dcs.triggers import TriggerStart, TriggerZone
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad from dcs.unit import Ship, Unit, Vehicle, InvisibleFARP
from dcs.unitgroup import ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, ShipType, VehicleType from dcs.unittype import StaticType, ShipType, VehicleType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
@ -589,22 +590,45 @@ class HelipadGenerator:
self.game = game self.game = game
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.tacan_registry = tacan_registry self.tacan_registry = tacan_registry
self.helipads: list[StaticGroup] = []
def generate(self) -> None: def generate(self) -> None:
# Note : Helipad are generated as neutral object in order not to interfer with capture triggers
neutral_country = self.m.country(self.game.neutral_country.name)
country = self.m.country(self.game.coalition_for(self.cp.captured).country_name) country = self.m.country(self.game.coalition_for(self.cp.captured).country_name)
for i, helipad in enumerate(self.cp.helipads): for i, helipad in enumerate(self.cp.helipads):
name = self.cp.name + "_helipad_" + str(i) name = self.cp.name + "_helipad_" + str(i)
logging.info("Generating helipad : " + name) logging.info("Generating helipad static : " + name)
pad = SingleHeliPad(name=(name + "_unit")) pad = InvisibleFARP(name=name)
pad.position = Point(helipad.x, helipad.y) pad.position = Point(helipad.x, helipad.y)
pad.heading = helipad.heading.degrees pad.heading = helipad.heading.degrees
# pad.heliport_frequency = self.radio_registry.alloc_uhf() TODO : alloc radio & callsign
sg = unitgroup.StaticGroup(self.m.next_group_id(), name) sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
sg.add_unit(pad) sg.add_unit(pad)
sp = StaticPoint() sp = StaticPoint()
sp.position = pad.position sp.position = pad.position
sg.add_point(sp) sg.add_point(sp)
country.add_static_group(sg) neutral_country.add_static_group(sg)
self.helipads.append(sg)
# Generate a FARP Ammo and Fuel stack for each pad
self.m.static_group(
country=country,
name=(name + "_fuel"),
_type=Fortification.FARP_Fuel_Depot,
position=pad.position.point_from_heading(helipad.heading.degrees, 35),
heading=pad.heading,
)
self.m.static_group(
country=country,
name=(name + "_ammo"),
_type=Fortification.FARP_Ammo_Dump_Coating,
position=pad.position.point_from_heading(
helipad.heading.degrees, 35
).point_from_heading(helipad.heading.degrees + 90, 10),
heading=pad.heading,
)
class GroundObjectsGenerator: class GroundObjectsGenerator:
@ -631,13 +655,18 @@ class GroundObjectsGenerator:
self.unit_map = unit_map self.unit_map = unit_map
self.icls_alloc = iter(range(1, 21)) self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {} self.runways: Dict[str, RunwayData] = {}
self.helipads: dict[ControlPoint, list[StaticGroup]] = defaultdict(list)
def generate(self) -> None: def generate(self) -> None:
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
country = self.m.country(self.game.coalition_for(cp.captured).country_name) country = self.m.country(self.game.coalition_for(cp.captured).country_name)
HelipadGenerator(
# Generate helipads
helipad_gen = HelipadGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry self.m, cp, self.game, self.radio_registry, self.tacan_registry
).generate() )
helipad_gen.generate()
self.helipads[cp] = helipad_gen.helipads
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
generator: GenericGroundObjectGenerator[Any] generator: GenericGroundObjectGenerator[Any]

View File

@ -40,12 +40,14 @@ class QAircraftRecruitmentMenu(UnitTransactionFrame[Squadron]):
row = 0 row = 0
unit_types: Set[AircraftType] = set() unit_types: Set[AircraftType] = set()
for squadron in cp.squadrons: for squadron in cp.squadrons:
unit_types.add(squadron.aircraft) unit_types.add(squadron.aircraft)
sorted_squadrons = sorted(cp.squadrons, key=lambda s: (s.aircraft.name, s.name)) sorted_squadrons = sorted(cp.squadrons, key=lambda s: (s.aircraft.name, s.name))
for row, squadron in enumerate(sorted_squadrons): for row, squadron in enumerate(sorted_squadrons):
self.add_purchase_row(squadron, task_box_layout, row) self.add_purchase_row(squadron, task_box_layout, row)
stretch = QVBoxLayout() stretch = QVBoxLayout()
stretch.addStretch() stretch.addStretch()
task_box_layout.addLayout(stretch, row, 0) task_box_layout.addLayout(stretch, row, 0)

View File

@ -1,11 +0,0 @@
{
"name": "Syria - Battle for Golan Heights",
"theater": "Syria",
"authors": "Khopa",
"recommended_player_faction": "Israel 2000",
"recommended_enemy_faction": "Syria 2011",
"description": "<p>In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.<br/><br/>This scenario is designed to be performance friendly.</p>",
"miz": "golan_heights_lite.miz",
"performance": 1,
"version": "8.0"
}

View File

@ -0,0 +1,104 @@
---
name: Syria - Battle for Golan Heights
theater: Syria
authors: Khopa
recommended_player_faction: Israel 2000
recommended_enemy_faction: Syria 2011
description: <p>In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.<br/><br/>This scenario is designed to be performance and helicopter friendly.</p>
miz: golan_heights_lite.miz
performance: 1
version: "9.0"
squadrons:
# Ramat-David
30:
- primary: AEW&C
aircraft:
- E-3A
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- primary: Transport
aircraft:
- C-130
- primary: BARCAP
secondary: any
aircraft:
- F-15C Eagle
- primary: BARCAP
secondary: any
aircraft:
- F-4E Phantom II
- primary: Strike
secondary: air-to-ground
aircraft:
- F-15E Strike Eagle
- primary: SEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
# Golan South
Golan South:
- primary: CAS
secondary: air-to-ground
aircraft:
- AH-1W SuperCobra
- primary: CAS
secondary: air-to-ground
aircraft:
- AH-64D Apache Longbow
- primary: Transport
aircraft:
- UH-1H Iroquois
# Golan North
Golan North:
- primary: CAS
secondary: air-to-ground
aircraft:
- Mi-24P Hind-F
- primary: CAS
aircraft:
- SA 342M Gazelle
- primary: Transport
secondary: air-to-ground
aircraft:
- Mi-8MTV2 Hip
# Marj Ruhayyil
23:
- primary: BARCAP
secondary: any
aircraft:
- MiG-21bis Fishbed-N
- primary: BARCAP
secondary: any
aircraft:
- MiG-23MLD Flogger-K
- primary: SEAD
secondary: air-to-ground
aircraft:
- Su-17M4 Fitter-K
- primary: Strike
secondary: air-to-ground
aircraft:
- Su-17M4 Fitter-K
# Damascus
7:
- primary: BARCAP
secondary: any
aircraft:
- MiG-29S Fulcrum-C
- primary: BARCAP
secondary: any
aircraft:
- MiG-21bis Fishbed-N
- primary: BARCAP
secondary: any
aircraft:
- MiG-25PD Foxbat-E
- primary: SEAD
secondary: air-to-ground
aircraft:
- Su-24M Fencer-D
- primary: Strike
secondary: air-to-ground
aircraft:
- Su-17M4 Fitter-K