diff --git a/game/data/weapons.py b/game/data/weapons.py
index 7aa21beb..668cc028 100644
--- a/game/data/weapons.py
+++ b/game/data/weapons.py
@@ -5,12 +5,12 @@ import inspect
import logging
from collections import defaultdict
from dataclasses import dataclass
-from typing import Dict, Iterator, Optional, Set, Tuple, Type, Union, cast
+from typing import Dict, Iterator, Optional, Set, Tuple, Union, cast
from dcs.unitgroup import FlyingGroup
-from dcs.unittype import FlyingType
from dcs.weapons_data import Weapons, weapon_ids
+from game.dcs.aircrafttype import AircraftType
PydcsWeapon = Dict[str, Union[int, str]]
PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
@@ -97,12 +97,12 @@ class Pylon:
yield weapon
@classmethod
- def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon:
+ def for_aircraft(cls, aircraft: AircraftType, number: int) -> Pylon:
# In pydcs these are all arbitrary inner classes of the aircraft type.
# The only way to identify them is by their name.
pylons = [
v
- for v in aircraft.__dict__.values()
+ for v in aircraft.dcs_unit_type.__dict__.values()
if inspect.isclass(v) and v.__name__.startswith("Pylon")
]
@@ -121,8 +121,8 @@ class Pylon:
return cls(number, allowed)
@classmethod
- def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]:
- for pylon in sorted(list(aircraft.pylons)):
+ def iter_pylons(cls, aircraft: AircraftType) -> Iterator[Pylon]:
+ for pylon in sorted(list(aircraft.dcs_unit_type.pylons)):
yield cls.for_aircraft(aircraft, pylon)
diff --git a/game/db.py b/game/db.py
index b87bdac4..d403f04e 100644
--- a/game/db.py
+++ b/game/db.py
@@ -133,7 +133,7 @@ from dcs.ships import (
from dcs.terrain.terrain import Airport
from dcs.unit import Ship, Unit, Vehicle
from dcs.unitgroup import ShipGroup, StaticGroup
-from dcs.unittype import FlyingType, UnitType, VehicleType
+from dcs.unittype import UnitType, VehicleType
from dcs.vehicles import (
AirDefence,
Armor,
@@ -803,44 +803,6 @@ REWARDS = {
"derrick": 8,
}
-CARRIER_CAPABLE = [
- FA_18C_hornet,
- F_14A_135_GR,
- F_14B,
- AV8BNA,
- Su_33,
- A_4E_C,
- S_3B,
- S_3B_Tanker,
- E_2C,
- UH_1H,
- Mi_8MT,
- Ka_50,
- AH_1W,
- OH_58D,
- UH_60A,
- SH_60B,
- SA342L,
- SA342M,
- SA342Minigun,
- SA342Mistral,
-]
-
-LHA_CAPABLE = [
- AV8BNA,
- UH_1H,
- Mi_8MT,
- Ka_50,
- AH_1W,
- OH_58D,
- UH_60A,
- SH_60B,
- SA342L,
- SA342M,
- SA342Minigun,
- SA342Mistral,
-]
-
"""
---------- END OF CONFIGURATION SECTION
"""
@@ -938,7 +900,9 @@ def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id
-def unit_get_expanded_info(country_name: str, unit_type, request_type: str) -> str:
+def unit_get_expanded_info(
+ country_name: str, unit_type: Type[UnitType], request_type: str
+) -> str:
original_name = unit_type.name and unit_type.name or unit_type.id
default_value = None
faction_value = None
@@ -980,13 +944,6 @@ def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
return None
-def flying_type_from_name(name: str) -> Optional[Type[FlyingType]]:
- unit_type = plane_map.get(name)
- if unit_type is not None:
- return unit_type
- return helicopter_map.get(name)
-
-
def unit_type_of(unit: Unit) -> UnitType:
if isinstance(unit, Vehicle):
return vehicle_map[unit.type]
@@ -1013,39 +970,3 @@ F_16C_50.Liveries = DefaultLiveries
P_51D_30_NA.Liveries = DefaultLiveries
Ju_88A4.Liveries = DefaultLiveries
B_17G.Liveries = DefaultLiveries
-
-# List of airframes that rely on their gun as a primary weapon. We confiscate bullets
-# from most AI air-to-ground missions since they aren't smart enough to RTB when they're
-# out of everything other than bullets (DCS does not have an all-but-gun winchester
-# option) and we don't want to be attacking fully functional Tors with a Vulcan.
-#
-# These airframes are the exceptions. They probably should be using their gun regardless
-# of the mission type.
-GUN_RELIANT_AIRFRAMES: List[Type[FlyingType]] = [
- AH_1W,
- AH_64A,
- AH_64D,
- A_10A,
- A_10C,
- A_10C_2,
- A_20G,
- Bf_109K_4,
- FW_190A8,
- FW_190D9,
- F_86F_Sabre,
- Ju_88A4,
- Ka_50,
- MiG_15bis,
- MiG_19P,
- Mi_24V,
- Mi_28N,
- P_47D_30,
- P_47D_30bl1,
- P_47D_40,
- P_51D,
- P_51D_30_NA,
- SpitfireLFMkIX,
- SpitfireLFMkIXCW,
- Su_25,
- Su_25T,
-]
diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py
new file mode 100644
index 00000000..5ca4f255
--- /dev/null
+++ b/game/dcs/aircrafttype.py
@@ -0,0 +1,177 @@
+from __future__ import annotations
+
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from functools import cached_property
+from pathlib import Path
+from typing import ClassVar, Type, Iterator, TYPE_CHECKING, Optional, Any
+
+import yaml
+from dcs.helicopters import helicopter_map
+from dcs.planes import plane_map
+from dcs.unittype import FlyingType
+
+from game.radio.channels import (
+ ChannelNamer,
+ RadioChannelAllocator,
+ CommonRadioChannelAllocator,
+)
+from game.utils import Speed, kph
+
+if TYPE_CHECKING:
+ from gen.aircraft import FlightData
+ from gen import AirSupport, RadioFrequency, RadioRegistry
+ from gen.radios import Radio
+
+
+@dataclass(frozen=True)
+class RadioConfig:
+ inter_flight: Optional[Radio]
+ intra_flight: Optional[Radio]
+ channel_allocator: Optional[RadioChannelAllocator]
+ channel_namer: Type[ChannelNamer]
+
+ @classmethod
+ def from_data(cls, data: dict[str, Any]) -> RadioConfig:
+ return RadioConfig(
+ cls.make_radio(data.get("inter_flight", None)),
+ cls.make_radio(data.get("intra_flight", None)),
+ cls.make_allocator(data.get("channels", {})),
+ cls.make_namer(data.get("channels", {})),
+ )
+
+ @classmethod
+ def make_radio(cls, name: Optional[str]) -> Optional[Radio]:
+ from gen.radios import get_radio
+
+ if name is None:
+ return None
+ return get_radio(name)
+
+ @classmethod
+ def make_allocator(cls, data: dict[str, Any]) -> Optional[RadioChannelAllocator]:
+ try:
+ alloc_type = data["type"]
+ except KeyError:
+ return None
+ return {"common": CommonRadioChannelAllocator}[alloc_type].from_cfg(data)
+
+ @classmethod
+ def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]:
+ return {"default": ChannelNamer}[config.get("namer", "default")]
+
+
+@dataclass(frozen=True)
+class AircraftType:
+ dcs_unit_type: Type[FlyingType]
+ name: str
+ description: str
+ price: int
+ carrier_capable: bool
+ lha_capable: bool
+ always_keeps_gun: bool
+ intra_flight_radio: Optional[Radio]
+ channel_allocator: Optional[RadioChannelAllocator]
+ channel_namer: Type[ChannelNamer]
+
+ _by_name: ClassVar[dict[str, AircraftType]] = {}
+ _by_unit_type: ClassVar[dict[Type[FlyingType], list[AircraftType]]] = defaultdict(
+ list
+ )
+ _loaded: ClassVar[bool] = False
+
+ def __str__(self) -> str:
+ return self.name
+
+ @property
+ def dcs_id(self) -> str:
+ return self.dcs_unit_type.id
+
+ @property
+ def flyable(self) -> bool:
+ return self.dcs_unit_type.flyable
+
+ @cached_property
+ def max_speed(self) -> Speed:
+ return kph(self.dcs_unit_type.max_speed)
+
+ def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
+ from gen.radios import ChannelInUseError, MHz
+
+ if self.intra_flight_radio is not None:
+ return radio_registry.alloc_for_radio(self.intra_flight_radio)
+
+ freq = MHz(self.dcs_unit_type.radio_frequency)
+ try:
+ radio_registry.reserve(freq)
+ except ChannelInUseError:
+ pass
+ return freq
+
+ def assign_channels_for_flight(
+ self, flight: FlightData, air_support: AirSupport
+ ) -> None:
+ if self.channel_allocator is not None:
+ self.channel_allocator.assign_channels_for_flight(flight, air_support)
+
+ def channel_name(self, radio_id: int, channel_id: int) -> str:
+ return self.channel_namer.channel_name(radio_id, channel_id)
+
+ @classmethod
+ def register(cls, aircraft_type: AircraftType) -> None:
+ cls._by_name[aircraft_type.name] = aircraft_type
+ cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
+
+ @classmethod
+ def named(cls, name: str) -> AircraftType:
+ if not cls._loaded:
+ cls._load_all()
+ return cls._by_name[name]
+
+ @classmethod
+ def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
+ yield from cls._by_unit_type[dcs_unit_type]
+
+ @staticmethod
+ def _each_unit_type() -> Iterator[Type[FlyingType]]:
+ yield from helicopter_map.values()
+ yield from plane_map.values()
+
+ @classmethod
+ def _load_all(cls) -> None:
+ for unit_type in cls._each_unit_type():
+ for data in cls._each_variant_of(unit_type):
+ cls.register(data)
+ cls._loaded = True
+
+ @classmethod
+ def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
+ data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
+ if not data_path.exists():
+ logging.warning(f"No data for {aircraft.id}; it will not be available")
+ return
+
+ with data_path.open() as data_file:
+ data = yaml.safe_load(data_file)
+
+ try:
+ price = data["price"]
+ except KeyError as ex:
+ raise KeyError(f"Missing required price field: {data_path}") from ex
+
+ radio_config = RadioConfig.from_data(data.get("radios", {}))
+
+ for variant in data.get("variants", [aircraft.id]):
+ yield AircraftType(
+ dcs_unit_type=aircraft,
+ name=variant,
+ description=data.get("description", "No data."),
+ price=price,
+ carrier_capable=data.get("carrier_capable", False),
+ lha_capable=data.get("lha_capable", False),
+ always_keeps_gun=data.get("always_keeps_gun", False),
+ intra_flight_radio=radio_config.intra_flight,
+ channel_allocator=radio_config.channel_allocator,
+ channel_namer=radio_config.channel_namer,
+ )
diff --git a/game/debriefing.py b/game/debriefing.py
index 88c9f8ae..e2166a74 100644
--- a/game/debriefing.py
+++ b/game/debriefing.py
@@ -18,9 +18,10 @@ from typing import (
TYPE_CHECKING,
)
-from dcs.unittype import FlyingType, UnitType
+from dcs.unittype import UnitType
from game import db
+from game.dcs.aircrafttype import AircraftType
from game.theater import Airfield, ControlPoint
from game.transfers import CargoShip
from game.unitmap import (
@@ -49,8 +50,8 @@ class AirLosses:
def losses(self) -> Iterator[FlyingUnit]:
return itertools.chain(self.player, self.enemy)
- def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
- losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
+ def by_type(self, player: bool) -> Dict[AircraftType, int]:
+ losses_by_type: Dict[AircraftType, int] = defaultdict(int)
losses = self.player if player else self.enemy
for loss in losses:
losses_by_type[loss.flight.unit_type] += 1
diff --git a/game/factions/faction.py b/game/factions/faction.py
index cdb75ed0..8a09807c 100644
--- a/game/factions/faction.py
+++ b/game/factions/faction.py
@@ -1,5 +1,4 @@
from __future__ import annotations
-from game.data.groundunitclass import GroundUnitClass
import logging
from dataclasses import dataclass, field
@@ -23,7 +22,9 @@ from game.data.doctrine import (
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
)
-from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
+from game.data.groundunitclass import GroundUnitClass
+from game.dcs.aircrafttype import AircraftType
+from pydcs_extensions.mod_units import MODDED_VEHICLES
@dataclass
@@ -45,13 +46,13 @@ class Faction:
description: str = field(default="")
# Available aircraft
- aircrafts: List[Type[FlyingType]] = field(default_factory=list)
+ aircrafts: List[AircraftType] = field(default_factory=list)
# Available awacs aircraft
- awacs: List[Type[FlyingType]] = field(default_factory=list)
+ awacs: List[AircraftType] = field(default_factory=list)
# Available tanker aircraft
- tankers: List[Type[FlyingType]] = field(default_factory=list)
+ tankers: List[AircraftType] = field(default_factory=list)
# Available frontline units
frontline_units: List[Type[VehicleType]] = field(default_factory=list)
@@ -114,7 +115,7 @@ class Faction:
has_jtac: bool = field(default=False)
# Unit to use as JTAC for this faction
- jtac_unit: Optional[Type[FlyingType]] = field(default=None)
+ jtac_unit: Optional[AircraftType] = field(default=None)
# doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
@@ -123,7 +124,7 @@ class Faction:
building_set: List[str] = field(default_factory=list)
# List of default livery overrides
- liveries_overrides: Dict[Type[UnitType], List[str]] = field(default_factory=dict)
+ liveries_overrides: Dict[AircraftType, List[str]] = field(default_factory=dict)
#: Set to True if the faction should force the "Unrestricted satnav" option
#: for the mission. This option enables GPS for capable aircraft regardless
@@ -163,9 +164,9 @@ class Faction:
faction.authors = json.get("authors", "")
faction.description = json.get("description", "")
- faction.aircrafts = load_all_aircraft(json.get("aircrafts", []))
- faction.awacs = load_all_aircraft(json.get("awacs", []))
- faction.tankers = load_all_aircraft(json.get("tankers", []))
+ faction.aircrafts = [AircraftType.named(n) for n in json.get("aircrafts", [])]
+ faction.awacs = [AircraftType.named(n) for n in json.get("awacs", [])]
+ faction.tankers = [AircraftType.named(n) for n in json.get("tankers", [])]
faction.aircrafts = list(
set(faction.aircrafts + faction.awacs + faction.tankers)
@@ -198,7 +199,7 @@ class Faction:
faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None)
if jtac_name is not None:
- faction.jtac_unit = load_aircraft(jtac_name)
+ faction.jtac_unit = AircraftType.named(jtac_name)
else:
faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1))
@@ -232,27 +233,14 @@ class Faction:
# Load liveries override
faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {})
- for k, v in liveries_overrides.items():
- k = load_aircraft(k)
- if k is not None:
- faction.liveries_overrides[k] = [s.lower() for s in v]
+ for name, livery in liveries_overrides.items():
+ aircraft = AircraftType.named(name)
+ faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
return faction
- @property
- def all_units(self) -> List[Type[UnitType]]:
- return (
- self.infantry_units
- + self.aircrafts
- + self.awacs
- + self.artillery_units
- + self.frontline_units
- + self.tankers
- + self.logistics_units
- )
-
@property
def ground_units(self) -> Iterator[Type[VehicleType]]:
yield from self.artillery_units
@@ -283,22 +271,6 @@ def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitTyp
return None
-def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
- return cast(
- Optional[FlyingType],
- unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]),
- )
-
-
-def load_all_aircraft(data) -> List[Type[FlyingType]]:
- items = []
- for name in data:
- item = load_aircraft(name)
- if item is not None:
- items.append(item)
- return items
-
-
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
return cast(
Optional[FlyingType],
diff --git a/game/inventory.py b/game/inventory.py
index 651b80a8..4014c05c 100644
--- a/game/inventory.py
+++ b/game/inventory.py
@@ -6,6 +6,7 @@ from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
from dcs.unittype import FlyingType
+from game.dcs.aircrafttype import AircraftType
from gen.flights.flight import Flight
if TYPE_CHECKING:
@@ -17,9 +18,9 @@ class ControlPointAircraftInventory:
def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point
- self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
+ self.inventory: Dict[AircraftType, int] = defaultdict(int)
- def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
+ def add_aircraft(self, aircraft: AircraftType, count: int) -> None:
"""Adds aircraft to the inventory.
Args:
@@ -28,7 +29,7 @@ class ControlPointAircraftInventory:
"""
self.inventory[aircraft] += count
- def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
+ def remove_aircraft(self, aircraft: AircraftType, count: int) -> None:
"""Removes aircraft from the inventory.
Args:
@@ -42,12 +43,12 @@ class ControlPointAircraftInventory:
available = self.inventory[aircraft]
if available < count:
raise ValueError(
- f"Cannot remove {count} {aircraft.id} from "
+ f"Cannot remove {count} {aircraft} from "
f"{self.control_point.name}. Only have {available}."
)
self.inventory[aircraft] -= count
- def available(self, aircraft: Type[FlyingType]) -> int:
+ def available(self, aircraft: AircraftType) -> int:
"""Returns the number of available aircraft of the given type.
Args:
@@ -59,14 +60,14 @@ class ControlPointAircraftInventory:
return 0
@property
- def types_available(self) -> Iterator[Type[FlyingType]]:
+ def types_available(self) -> Iterator[AircraftType]:
"""Iterates over all available aircraft types."""
for aircraft, count in self.inventory.items():
if count > 0:
yield aircraft
@property
- def all_aircraft(self) -> Iterator[Tuple[Type[FlyingType], int]]:
+ def all_aircraft(self) -> Iterator[Tuple[AircraftType, int]]:
"""Iterates over all available aircraft types, including amounts."""
for aircraft, count in self.inventory.items():
if count > 0:
@@ -107,9 +108,9 @@ class GlobalAircraftInventory:
return self.inventories[control_point]
@property
- def available_types_for_player(self) -> Iterator[Type[FlyingType]]:
+ def available_types_for_player(self) -> Iterator[AircraftType]:
"""Iterates over all aircraft types available to the player."""
- seen: Set[Type[FlyingType]] = set()
+ seen: Set[AircraftType] = set()
for control_point, inventory in self.inventories.items():
if control_point.captured:
for aircraft in inventory.types_available:
diff --git a/game/operation/operation.py b/game/operation/operation.py
index 2f683f78..5a4f7c3b 100644
--- a/game/operation/operation.py
+++ b/game/operation/operation.py
@@ -17,7 +17,7 @@ from dcs.triggers import TriggerStart
from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import TheaterGroundObject
from gen import Conflict, FlightType, VisualGenerator
-from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
+from gen.aircraft import AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo
@@ -215,23 +215,7 @@ class Operation:
for flight in flights:
if not flight.client_units:
continue
- cls.assign_channels_to_flight(flight, air_support)
-
- @staticmethod
- def assign_channels_to_flight(flight: FlightData, air_support: AirSupport) -> None:
- """Assigns preset radio channels for a client flight."""
- airframe = flight.aircraft_type
-
- try:
- aircraft_data = AIRCRAFT_DATA[airframe.id]
- except KeyError:
- logging.warning(f"No aircraft data for {airframe.id}")
- return
-
- if aircraft_data.channel_allocator is not None:
- aircraft_data.channel_allocator.assign_channels_for_flight(
- flight, air_support
- )
+ flight.aircraft_type.assign_channels_for_flight(flight, air_support)
@classmethod
def _create_tacan_registry(
diff --git a/game/procurement.py b/game/procurement.py
index 021014ce..644d9c0b 100644
--- a/game/procurement.py
+++ b/game/procurement.py
@@ -9,6 +9,7 @@ from dcs.unittype import FlyingType, VehicleType
from game import db
from game.data.groundunitclass import GroundUnitClass
+from game.dcs.aircrafttype import AircraftType
from game.factions.faction import Faction
from game.theater import ControlPoint, MissionTarget
from game.utils import Distance
@@ -113,7 +114,7 @@ class ProcurementAi:
if available % 2 == 0:
continue
inventory.remove_aircraft(aircraft, 1)
- total += db.PRICES[aircraft]
+ total += aircraft.price
return total
def repair_runways(self, budget: float) -> float:
@@ -198,12 +199,12 @@ class ProcurementAi:
airbase: ControlPoint,
number: int,
max_price: float,
- ) -> Optional[Type[FlyingType]]:
- best_choice: Optional[Type[FlyingType]] = None
+ ) -> Optional[AircraftType]:
+ best_choice: Optional[AircraftType] = None
for unit in aircraft_for_task(task):
if unit not in self.faction.aircrafts:
continue
- if db.PRICES[unit] * number > max_price:
+ if unit.price * number > max_price:
continue
if not airbase.can_operate(unit):
continue
@@ -224,7 +225,7 @@ class ProcurementAi:
def affordable_aircraft_for(
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
- ) -> Optional[Type[FlyingType]]:
+ ) -> Optional[AircraftType]:
return self._affordable_aircraft_for_task(
request.task_capability, airbase, request.number, budget
)
@@ -242,7 +243,7 @@ class ProcurementAi:
# able to operate expensive aircraft.
continue
- budget -= db.PRICES[unit] * request.number
+ budget -= unit.price * request.number
airbase.pending_unit_deliveries.order({unit: request.number})
return budget, True
return budget, False
diff --git a/game/radio/channels.py b/game/radio/channels.py
new file mode 100644
index 00000000..83df8e6c
--- /dev/null
+++ b/game/radio/channels.py
@@ -0,0 +1,298 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Optional, Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from gen import FlightData, AirSupport
+
+
+class RadioChannelAllocator:
+ """Base class for radio channel allocators."""
+
+ def assign_channels_for_flight(
+ self, flight: FlightData, air_support: AirSupport
+ ) -> None:
+ """Assigns mission frequencies to preset channels for the flight."""
+ raise NotImplementedError
+
+ @classmethod
+ def from_cfg(cls, cfg: dict[str, Any]) -> RadioChannelAllocator:
+ return cls()
+
+ @classmethod
+ def name(cls) -> str:
+ raise NotImplementedError
+
+
+@dataclass(frozen=True)
+class CommonRadioChannelAllocator(RadioChannelAllocator):
+ """Radio channel allocator suitable for most aircraft.
+
+ Most of the aircraft with preset channels available have one or more radios
+ with 20 or more channels available (typically per-radio, but this is not the
+ case for the JF-17).
+ """
+
+ #: Index of the radio used for intra-flight communications. Matches the
+ #: index of the panel_radio field of the pydcs.dcs.planes object.
+ inter_flight_radio_index: Optional[int]
+
+ #: Index of the radio used for intra-flight communications. Matches the
+ #: index of the panel_radio field of the pydcs.dcs.planes object.
+ intra_flight_radio_index: Optional[int]
+
+ def assign_channels_for_flight(
+ self, flight: FlightData, air_support: AirSupport
+ ) -> None:
+ if self.intra_flight_radio_index is not None:
+ flight.assign_channel(
+ self.intra_flight_radio_index, 1, flight.intra_flight_channel
+ )
+
+ if self.inter_flight_radio_index is None:
+ return
+
+ # For cases where the inter-flight and intra-flight radios share presets
+ # (the JF-17 only has one set of channels, even though it can use two
+ # channels simultaneously), start assigning inter-flight channels at 2.
+ radio_id = self.inter_flight_radio_index
+ if self.intra_flight_radio_index == radio_id:
+ first_channel = 2
+ else:
+ first_channel = 1
+
+ last_channel = flight.num_radio_channels(radio_id)
+ channel_alloc = iter(range(first_channel, last_channel + 1))
+
+ if flight.departure.atc is not None:
+ flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
+
+ # TODO: If there ever are multiple AWACS, limit to mission relevant.
+ for awacs in air_support.awacs:
+ flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
+
+ if flight.arrival != flight.departure and flight.arrival.atc is not None:
+ flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
+
+ try:
+ # TODO: Skip incompatible tankers.
+ for tanker in air_support.tankers:
+ flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
+
+ if flight.divert is not None and flight.divert.atc is not None:
+ flight.assign_channel(radio_id, next(channel_alloc), flight.divert.atc)
+ except StopIteration:
+ # Any remaining channels are nice-to-haves, but not necessary for
+ # the few aircraft with a small number of channels available.
+ pass
+
+ @classmethod
+ def from_cfg(cls, cfg: dict[str, Any]) -> CommonRadioChannelAllocator:
+ return CommonRadioChannelAllocator(
+ inter_flight_radio_index=cfg["inter_flight_radio_index"],
+ intra_flight_radio_index=cfg["intra_flight_radio_index"],
+ )
+
+ @classmethod
+ def name(cls) -> str:
+ return "common"
+
+
+@dataclass(frozen=True)
+class NoOpChannelAllocator(RadioChannelAllocator):
+ """Channel allocator for aircraft that don't support preset channels."""
+
+ def assign_channels_for_flight(
+ self, flight: FlightData, air_support: AirSupport
+ ) -> None:
+ pass
+
+ @classmethod
+ def name(cls) -> str:
+ return "noop"
+
+
+@dataclass(frozen=True)
+class FarmerRadioChannelAllocator(RadioChannelAllocator):
+ """Preset channel allocator for the MiG-19P."""
+
+ def assign_channels_for_flight(
+ self, flight: FlightData, air_support: AirSupport
+ ) -> None:
+ # The Farmer only has 6 preset channels. It also only has a VHF radio,
+ # and currently our ATC data and AWACS are only in the UHF band.
+ radio_id = 1
+ flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
+ # TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
+ # TODO: Assign 2 and 3 to AWACS if it is VHF.
+
+ @classmethod
+ def name(cls) -> str:
+ return "farmer"
+
+
+@dataclass(frozen=True)
+class ViggenRadioChannelAllocator(RadioChannelAllocator):
+ """Preset channel allocator for the AJS37."""
+
+ def assign_channels_for_flight(
+ self, flight: FlightData, air_support: AirSupport
+ ) -> None:
+ # The Viggen's preset channels are handled differently from other
+ # aircraft. The aircraft automatically configures channels for every
+ # allied flight in the game (including AWACS) and for every airfield. As
+ # such, we don't need to allocate any of those. There are seven presets
+ # we can modify, however: three channels for the main radio intended for
+ # communication with wingmen, and four emergency channels for the backup
+ # radio. We'll set the first channel of the main radio to the
+ # intra-flight channel, and the first three emergency channels to each
+ # of the flight plan's airfields. The fourth emergency channel is always
+ # the guard channel.
+ radio_id = 1
+ flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
+ if flight.departure.atc is not None:
+ flight.assign_channel(radio_id, 4, flight.departure.atc)
+ if flight.arrival.atc is not None:
+ flight.assign_channel(radio_id, 5, flight.arrival.atc)
+ # TODO: Assign divert to 6 when we support divert airfields.
+
+ @classmethod
+ def name(cls) -> str:
+ return "viggen"
+
+
+@dataclass(frozen=True)
+class SCR522RadioChannelAllocator(RadioChannelAllocator):
+ """Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
+
+ def assign_channels_for_flight(
+ self, flight: FlightData, air_support: AirSupport
+ ) -> None:
+ radio_id = 1
+ flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
+ if flight.departure.atc is not None:
+ flight.assign_channel(radio_id, 2, flight.departure.atc)
+ if flight.arrival.atc is not None:
+ flight.assign_channel(radio_id, 3, flight.arrival.atc)
+
+ # TODO : Some GCI on Channel 4 ?
+
+ @classmethod
+ def name(cls) -> str:
+ return "SCR-522"
+
+
+class ChannelNamer:
+ """Base class allowing channel name customization per-aircraft.
+
+ Most aircraft will want to customize this behavior, but the default is
+ reasonable for any aircraft with numbered radios.
+ """
+
+ @staticmethod
+ def channel_name(radio_id: int, channel_id: int) -> str:
+ """Returns the name of the channel for the given radio and channel."""
+ return f"COMM{radio_id} Ch {channel_id}"
+
+ @classmethod
+ def name(cls) -> str:
+ return "default"
+
+
+class SingleRadioChannelNamer(ChannelNamer):
+ """Channel namer for the aircraft with only a single radio.
+
+ Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
+ it's not necessary for us to name the radio when naming the channel.
+ """
+
+ @staticmethod
+ def channel_name(radio_id: int, channel_id: int) -> str:
+ return f"Ch {channel_id}"
+
+ @classmethod
+ def name(cls) -> str:
+ return "single"
+
+
+class HueyChannelNamer(ChannelNamer):
+ """Channel namer for the UH-1H."""
+
+ @staticmethod
+ def channel_name(radio_id: int, channel_id: int) -> str:
+ return f"COM3 Ch {channel_id}"
+
+ @classmethod
+ def name(cls) -> str:
+ return "huey"
+
+
+class MirageChannelNamer(ChannelNamer):
+ """Channel namer for the M-2000."""
+
+ @staticmethod
+ def channel_name(radio_id: int, channel_id: int) -> str:
+ radio_name = ["V/UHF", "UHF"][radio_id - 1]
+ return f"{radio_name} Ch {channel_id}"
+
+ @classmethod
+ def name(cls) -> str:
+ return "mirage"
+
+
+class TomcatChannelNamer(ChannelNamer):
+ """Channel namer for the F-14."""
+
+ @staticmethod
+ def channel_name(radio_id: int, channel_id: int) -> str:
+ radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
+ return f"{radio_name} Ch {channel_id}"
+
+ @classmethod
+ def name(cls) -> str:
+ return "tomcat"
+
+
+class ViggenChannelNamer(ChannelNamer):
+ """Channel namer for the AJS37."""
+
+ @staticmethod
+ def channel_name(radio_id: int, channel_id: int) -> str:
+ if channel_id >= 4:
+ channel_letter = "EFGH"[channel_id - 4]
+ return f"FR 24 {channel_letter}"
+ return f"FR 22 Special {channel_id}"
+
+ @classmethod
+ def name(cls) -> str:
+ return "viggen"
+
+
+class ViperChannelNamer(ChannelNamer):
+ """Channel namer for the F-16."""
+
+ @staticmethod
+ def channel_name(radio_id: int, channel_id: int) -> str:
+ return f"COM{radio_id} Ch {channel_id}"
+
+ @classmethod
+ def name(cls) -> str:
+ return "viper"
+
+
+class SCR522ChannelNamer(ChannelNamer):
+ """
+ Channel namer for P-51 & P-47D
+ """
+
+ @staticmethod
+ def channel_name(radio_id: int, channel_id: int) -> str:
+ if channel_id > 3:
+ return "?"
+ else:
+ return f"Button " + "ABCD"[channel_id - 1]
+
+ @classmethod
+ def name(cls) -> str:
+ return "SCR-522"
diff --git a/game/squadrons.py b/game/squadrons.py
index 4e550465..1cee891f 100644
--- a/game/squadrons.py
+++ b/game/squadrons.py
@@ -8,7 +8,6 @@ from dataclasses import dataclass, field
from enum import unique, Enum
from pathlib import Path
from typing import (
- Type,
Tuple,
TYPE_CHECKING,
Optional,
@@ -17,10 +16,9 @@ from typing import (
)
import yaml
-from dcs.unittype import FlyingType
from faker import Faker
-from game.db import flying_type_from_name
+from game.dcs.aircrafttype import AircraftType
from game.settings import AutoAtoBehavior
if TYPE_CHECKING:
@@ -79,7 +77,7 @@ class Squadron:
nickname: str
country: str
role: str
- aircraft: Type[FlyingType]
+ aircraft: AircraftType
livery: Optional[str]
mission_types: tuple[FlightType, ...]
pilots: list[Pilot]
@@ -196,9 +194,11 @@ class Squadron:
with path.open() as squadron_file:
data = yaml.safe_load(squadron_file)
- unit_type = flying_type_from_name(data["aircraft"])
- if unit_type is None:
- raise KeyError(f"Could not find any aircraft with the ID {unit_type}")
+ name = data["aircraft"]
+ try:
+ unit_type = AircraftType.named(name)
+ except KeyError as ex:
+ raise KeyError(f"Could not find any aircraft named {name}") from ex
pilots = [Pilot(n, player=False) for n in data.get("pilots", [])]
pilots.extend([Pilot(n, player=True) for n in data.get("players", [])])
@@ -245,8 +245,8 @@ class SquadronLoader:
yield Path(persistency.base_path()) / "Liberation/Squadrons"
yield Path("resources/squadrons")
- def load(self) -> dict[Type[FlyingType], list[Squadron]]:
- squadrons: dict[Type[FlyingType], list[Squadron]] = defaultdict(list)
+ def load(self) -> dict[AircraftType, list[Squadron]]:
+ squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
country = self.game.country_for(self.player)
faction = self.game.faction_for(self.player)
any_country = country.startswith("Combined Joint Task Forces ")
@@ -317,7 +317,7 @@ class AirWing:
)
]
- def squadrons_for(self, aircraft: Type[FlyingType]) -> Sequence[Squadron]:
+ def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
return self.squadrons[aircraft]
def squadrons_for_task(self, task: FlightType) -> Iterator[Squadron]:
@@ -325,7 +325,7 @@ class AirWing:
if task in squadron.mission_types:
yield squadron
- def squadron_for(self, aircraft: Type[FlyingType]) -> Squadron:
+ def squadron_for(self, aircraft: AircraftType) -> Squadron:
return self.squadrons_for(aircraft)[0]
def iter_squadrons(self) -> Iterator[Squadron]:
diff --git a/game/theater/base.py b/game/theater/base.py
index 15d14f85..2fe84981 100644
--- a/game/theater/base.py
+++ b/game/theater/base.py
@@ -3,9 +3,10 @@ import logging
import typing
from typing import Dict, Type
-from dcs.unittype import FlyingType, VehicleType, UnitType
+from dcs.unittype import VehicleType
from game.db import PRICES
+from game.dcs.aircrafttype import AircraftType
BASE_MAX_STRENGTH = 1
BASE_MIN_STRENGTH = 0
@@ -13,7 +14,7 @@ BASE_MIN_STRENGTH = 0
class Base:
def __init__(self):
- self.aircraft: Dict[Type[FlyingType], int] = {}
+ self.aircraft: Dict[AircraftType, int] = {}
self.armor: Dict[Type[VehicleType], int] = {}
self.strength = 1
@@ -35,7 +36,7 @@ class Base:
logging.exception(f"No price found for {unit_type.id}")
return total
- def total_units_of_type(self, unit_type) -> int:
+ def total_units_of_type(self, unit_type: typing.Any) -> int:
return sum(
[
c
@@ -44,15 +45,16 @@ class Base:
]
)
- def commission_units(self, units: typing.Dict[typing.Type[UnitType], int]):
+ def commission_units(self, units: typing.Dict[typing.Any, int]):
for unit_type, unit_count in units.items():
if unit_count <= 0:
continue
- if issubclass(unit_type, VehicleType):
- target_dict = self.armor
- elif issubclass(unit_type, FlyingType):
+ target_dict: dict[typing.Any, int]
+ if isinstance(unit_type, AircraftType):
target_dict = self.aircraft
+ elif issubclass(unit_type, VehicleType):
+ target_dict = self.armor
else:
logging.error(
f"Unexpected unit type of {unit_type}: "
@@ -66,21 +68,22 @@ class Base:
for unit_type, count in units_lost.items():
+ target_dict: dict[typing.Any, int]
if unit_type in self.aircraft:
- target_array = self.aircraft
+ target_dict = self.aircraft
elif unit_type in self.armor:
- target_array = self.armor
+ target_dict = self.armor
else:
print("Base didn't find event type {}".format(unit_type))
continue
- if unit_type not in target_array:
+ if unit_type not in target_dict:
print("Base didn't find event type {}".format(unit_type))
continue
- target_array[unit_type] = max(target_array[unit_type] - count, 0)
- if target_array[unit_type] == 0:
- del target_array[unit_type]
+ target_dict[unit_type] = max(target_dict[unit_type] - count, 0)
+ if target_dict[unit_type] == 0:
+ del target_dict[unit_type]
def affect_strength(self, amount):
self.strength += amount
diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py
index 83d987e7..73dc7d0a 100644
--- a/game/theater/controlpoint.py
+++ b/game/theater/controlpoint.py
@@ -32,7 +32,7 @@ from dcs.ships import (
)
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit
-from dcs.unittype import FlyingType, VehicleType
+from dcs.unittype import VehicleType
from game import db
from game.point_with_heading import PointWithHeading
@@ -47,6 +47,7 @@ from .theatergroundobject import (
TheaterGroundObject,
)
from ..db import PRICES
+from ..dcs.aircrafttype import AircraftType
from ..utils import nautical_miles
from ..weather import Conditions
@@ -125,19 +126,19 @@ class PresetLocations:
@dataclass(frozen=True)
class AircraftAllocations:
- present: dict[Type[FlyingType], int]
- ordered: dict[Type[FlyingType], int]
- transferring: dict[Type[FlyingType], int]
+ present: dict[AircraftType, int]
+ ordered: dict[AircraftType, int]
+ transferring: dict[AircraftType, int]
@property
def total_value(self) -> int:
total: int = 0
for unit_type, count in self.present.items():
- total += PRICES[unit_type] * count
+ total += unit_type.price * count
for unit_type, count in self.ordered.items():
- total += PRICES[unit_type] * count
+ total += unit_type.price * count
for unit_type, count in self.transferring.items():
- total += PRICES[unit_type] * count
+ total += unit_type.price * count
return total
@@ -544,24 +545,16 @@ class ControlPoint(MissionTarget, ABC):
destination.control_point.base.commission_units({unit_type: 1})
destination = heapq.heappushpop(destinations, destination)
- def capture_aircraft(
- self, game: Game, airframe: Type[FlyingType], count: int
- ) -> None:
- try:
- value = PRICES[airframe] * count
- except KeyError:
- logging.exception(f"Unknown price for {airframe.id}")
- return
-
+ def capture_aircraft(self, game: Game, airframe: AircraftType, count: int) -> None:
+ value = airframe.price * count
game.adjust_budget(value, player=not self.captured)
game.message(
- f"No valid retreat destination in range of {self.name} for "
- f"{airframe.id}. {count} aircraft have been captured and sold for "
- f"${value}M."
+ f"No valid retreat destination in range of {self.name} for {airframe}"
+ f"{count} aircraft have been captured and sold for ${value}M."
)
def aircraft_retreat_destination(
- self, game: Game, airframe: Type[FlyingType]
+ self, game: Game, airframe: AircraftType
) -> Optional[ControlPoint]:
closest = ObjectiveDistanceCache.get_closest_airfields(self)
# TODO: Should be airframe dependent.
@@ -579,10 +572,10 @@ class ControlPoint(MissionTarget, ABC):
return None
def _retreat_air_units(
- self, game: Game, airframe: Type[FlyingType], count: int
+ self, game: Game, airframe: AircraftType, count: int
) -> None:
while count:
- logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
+ logging.debug(f"Retreating {count} {airframe} from {self.name}")
destination = self.aircraft_retreat_destination(game, airframe)
if destination is None:
self.capture_aircraft(game, airframe, count)
@@ -618,16 +611,16 @@ class ControlPoint(MissionTarget, ABC):
self.base.set_strength_to_minimum()
@abstractmethod
- def can_operate(self, aircraft: Type[FlyingType]) -> bool:
+ def can_operate(self, aircraft: AircraftType) -> bool:
...
- def aircraft_transferring(self, game: Game) -> dict[Type[FlyingType], int]:
+ def aircraft_transferring(self, game: Game) -> dict[AircraftType, int]:
if self.captured:
ato = game.blue_ato
else:
ato = game.red_ato
- transferring: defaultdict[Type[FlyingType], int] = defaultdict(int)
+ transferring: defaultdict[AircraftType, int] = defaultdict(int)
for package in ato.packages:
for flight in package.flights:
if flight.departure == flight.arrival:
@@ -692,7 +685,7 @@ class ControlPoint(MissionTarget, ABC):
def allocated_aircraft(self, game: Game) -> AircraftAllocations:
on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items():
- if issubclass(unit_bought, FlyingType):
+ if isinstance(unit_bought, AircraftType):
on_order[unit_bought] = count
return AircraftAllocations(
@@ -704,7 +697,7 @@ class ControlPoint(MissionTarget, ABC):
) -> GroundUnitAllocations:
on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items():
- if issubclass(unit_bought, VehicleType):
+ if type(unit_bought) == type and issubclass(unit_bought, VehicleType):
on_order[unit_bought] = count
transferring: dict[Type[VehicleType], int] = defaultdict(int)
@@ -788,7 +781,7 @@ class Airfield(ControlPoint):
self.airport = airport
self._runway_status = RunwayStatus()
- def can_operate(self, aircraft: FlyingType) -> bool:
+ def can_operate(self, aircraft: AircraftType) -> bool:
# TODO: Allow helicopters.
# Need to implement ground spawns so the helos don't use the runway.
# TODO: Allow harrier.
@@ -972,8 +965,8 @@ class Carrier(NavalControlPoint):
def is_carrier(self):
return True
- def can_operate(self, aircraft: FlyingType) -> bool:
- return aircraft in db.CARRIER_CAPABLE
+ def can_operate(self, aircraft: AircraftType) -> bool:
+ return aircraft.carrier_capable
@property
def total_aircraft_parking(self) -> int:
@@ -1006,8 +999,8 @@ class Lha(NavalControlPoint):
def is_lha(self) -> bool:
return True
- def can_operate(self, aircraft: FlyingType) -> bool:
- return aircraft in db.LHA_CAPABLE
+ def can_operate(self, aircraft: AircraftType) -> bool:
+ return aircraft.lha_capable
@property
def total_aircraft_parking(self) -> int:
@@ -1046,7 +1039,7 @@ class OffMapSpawn(ControlPoint):
def total_aircraft_parking(self) -> int:
return 1000
- def can_operate(self, aircraft: FlyingType) -> bool:
+ def can_operate(self, aircraft: AircraftType) -> bool:
return True
@property
@@ -1117,7 +1110,7 @@ class Fob(ControlPoint):
def total_aircraft_parking(self) -> int:
return 0
- def can_operate(self, aircraft: FlyingType) -> bool:
+ def can_operate(self, aircraft: AircraftType) -> bool:
return False
@property
diff --git a/game/transfers.py b/game/transfers.py
index 8d26113a..0a356047 100644
--- a/game/transfers.py
+++ b/game/transfers.py
@@ -18,8 +18,9 @@ from typing import (
)
from dcs.mapping import Point
-from dcs.unittype import FlyingType, VehicleType
+from dcs.unittype import VehicleType
+from game.dcs.aircrafttype import AircraftType
from game.procurement import AircraftProcurementRequest
from game.squadrons import Squadron
from game.theater import ControlPoint, MissionTarget
@@ -29,7 +30,7 @@ from game.theater.transitnetwork import (
)
from game.utils import meters, nautical_miles
from gen.ato import Package
-from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE
+from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE, aircraft_for_task
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight, FlightType
from gen.flights.flightplan import FlightPlanBuilder
@@ -191,9 +192,9 @@ class AirliftPlanner:
self.package = Package(target=next_stop, auto_asap=True)
def compatible_with_mission(
- self, unit_type: Type[FlyingType], airfield: ControlPoint
+ self, unit_type: AircraftType, airfield: ControlPoint
) -> bool:
- if not unit_type in TRANSPORT_CAPABLE:
+ if unit_type not in aircraft_for_task(FlightType.TRANSPORT):
return False
if not self.transfer.origin.can_operate(unit_type):
return False
@@ -201,7 +202,7 @@ class AirliftPlanner:
return False
# Cargo planes have no maximum range.
- if not unit_type.helicopter:
+ if not unit_type.dcs_unit_type.helicopter:
return True
# A helicopter that is transport capable and able to operate at both bases. Need
@@ -254,9 +255,11 @@ class AirliftPlanner:
self, squadron: Squadron, inventory: ControlPointAircraftInventory
) -> int:
available = inventory.available(squadron.aircraft)
- capacity_each = 1 if squadron.aircraft.helicopter else 2
+ capacity_each = 1 if squadron.aircraft.dcs_unit_type.helicopter else 2
required = math.ceil(self.transfer.size / capacity_each)
- flight_size = min(required, available, squadron.aircraft.group_size_max)
+ flight_size = min(
+ required, available, squadron.aircraft.dcs_unit_type.group_size_max
+ )
capacity = flight_size * capacity_each
if capacity < self.transfer.size:
diff --git a/game/unitdelivery.py b/game/unitdelivery.py
index fd250825..06a4f7ed 100644
--- a/game/unitdelivery.py
+++ b/game/unitdelivery.py
@@ -3,12 +3,13 @@ from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
-from typing import Dict, Optional, TYPE_CHECKING, Type
+from typing import Dict, Optional, TYPE_CHECKING, Type, Any
from dcs.unittype import UnitType, VehicleType
from game.theater import ControlPoint
from .db import PRICES
+from .dcs.aircrafttype import AircraftType
from .theater.transitnetwork import (
NoPathError,
TransitNetwork,
@@ -24,21 +25,24 @@ class GroundUnitSource:
control_point: ControlPoint
+AircraftOrVehicleType = Any
+
+
class PendingUnitDeliveries:
def __init__(self, destination: ControlPoint) -> None:
self.destination = destination
# Maps unit type to order quantity.
- self.units: Dict[Type[UnitType], int] = defaultdict(int)
+ self.units: Dict[AircraftOrVehicleType, int] = defaultdict(int)
def __str__(self) -> str:
return f"Pending delivery to {self.destination}"
- def order(self, units: Dict[Type[UnitType], int]) -> None:
+ def order(self, units: Dict[AircraftOrVehicleType, int]) -> None:
for k, v in units.items():
self.units[k] += v
- def sell(self, units: Dict[Type[UnitType], int]) -> None:
+ def sell(self, units: Dict[AircraftOrVehicleType, int]) -> None:
for k, v in units.items():
self.units[k] -= v
@@ -57,13 +61,13 @@ class PendingUnitDeliveries:
logging.info(f"Refunding {count} {unit_type.id} at {self.destination.name}")
game.adjust_budget(price * count, player=self.destination.captured)
- def pending_orders(self, unit_type: Type[UnitType]) -> int:
+ def pending_orders(self, unit_type: AircraftOrVehicleType) -> int:
pending_units = self.units.get(unit_type)
if pending_units is None:
pending_units = 0
return pending_units
- def available_next_turn(self, unit_type: Type[UnitType]) -> int:
+ def available_next_turn(self, unit_type: AircraftOrVehicleType) -> int:
current_units = self.destination.base.total_units_of_type(unit_type)
return self.pending_orders(unit_type) + current_units
@@ -77,15 +81,20 @@ class PendingUnitDeliveries:
self.refund_all(game)
return
- bought_units: Dict[Type[UnitType], int] = {}
+ bought_units: Dict[AircraftOrVehicleType, int] = {}
units_needing_transfer: Dict[Type[VehicleType], int] = {}
- sold_units: Dict[Type[UnitType], int] = {}
+ sold_units: Dict[AircraftOrVehicleType, int] = {}
for unit_type, count in self.units.items():
coalition = "Ally" if self.destination.captured else "Enemy"
- name = unit_type.id
+
+ if isinstance(unit_type, AircraftType):
+ name = unit_type.name
+ else:
+ name = unit_type.id
if (
- issubclass(unit_type, VehicleType)
+ type(unit_type) == type
+ and issubclass(unit_type, VehicleType)
and self.destination != ground_unit_source
):
source = ground_unit_source
diff --git a/gen/aircraft.py b/gen/aircraft.py
index 72d279fa..e20d0f89 100644
--- a/gen/aircraft.py
+++ b/gen/aircraft.py
@@ -12,30 +12,17 @@ from dcs.action import AITaskPush, ActivateGroup
from dcs.condition import CoalitionHasAirdrome, TimeAfter
from dcs.country import Country
from dcs.flyingunit import FlyingUnit
-from dcs.helicopters import UH_1H, helicopter_map
from dcs.mapping import Point
from dcs.mission import Mission, StartType
from dcs.planes import (
AJS37,
B_17G,
B_52H,
- Bf_109K_4,
C_101CC,
C_101EB,
- FW_190A8,
- FW_190D9,
F_14B,
- I_16,
JF_17,
- Ju_88A4,
- P_47D_30,
- P_47D_30bl1,
- P_47D_40,
- P_51D,
- P_51D_30_NA,
PlaneType,
- SpitfireLFMkIX,
- SpitfireLFMkIXCW,
Su_33,
Tu_22M3,
)
@@ -76,12 +63,12 @@ from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unit import Unit, Skill
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
-from dcs.unittype import FlyingType, UnitType
+from dcs.unittype import FlyingType
from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.data.weapons import Pylon
-from game.db import GUN_RELIANT_AIRFRAMES
+from game.dcs.aircrafttype import AircraftType
from game.factions.faction import Faction
from game.settings import Settings
from game.squadrons import Pilot
@@ -105,7 +92,7 @@ from gen.flights.flight import (
FlightWaypoint,
FlightWaypointType,
)
-from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
+from gen.radios import RadioFrequency, RadioRegistry
from gen.runways import RunwayData
from gen.tacan import TacanBand, TacanRegistry
from .airsupportgen import AirSupport, AwacsInfo, TankerInfo
@@ -131,16 +118,6 @@ RTB_ALTITUDE = meters(800)
RTB_DISTANCE = 5000
HELI_ALT = 500
-# Note that fallback radio channels will *not* be reserved. It's possible that
-# flights using these will overlap with other channels. This is because we would
-# need to make sure we fell back to a frequency that is not used by any beacon
-# or ATC, which we don't have the information to predict. Deal with the minor
-# annoyance for now since we'll be fleshing out radio info soon enough.
-ALLIES_WW2_CHANNEL = MHz(124)
-GERMAN_WW2_CHANNEL = MHz(40)
-HELICOPTER_CHANNEL = MHz(127)
-UHF_FALLBACK_CHANNEL = MHz(251)
-
TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_GROUP_LOC,
FlightWaypointType.TARGET_POINT,
@@ -148,121 +125,6 @@ TARGET_WAYPOINTS = (
)
-# TODO: Get radio information for all the special cases.
-def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
- if unit_type in helicopter_map.values() and unit_type != UH_1H:
- return HELICOPTER_CHANNEL
-
- german_ww2_aircraft = [
- Bf_109K_4,
- FW_190A8,
- FW_190D9,
- Ju_88A4,
- ]
-
- if unit_type in german_ww2_aircraft:
- return GERMAN_WW2_CHANNEL
-
- allied_ww2_aircraft = [
- I_16,
- P_47D_30,
- P_47D_30bl1,
- P_47D_40,
- P_51D,
- P_51D_30_NA,
- SpitfireLFMkIX,
- SpitfireLFMkIXCW,
- ]
-
- if unit_type in allied_ww2_aircraft:
- return ALLIES_WW2_CHANNEL
-
- return UHF_FALLBACK_CHANNEL
-
-
-class ChannelNamer:
- """Base class allowing channel name customization per-aircraft.
-
- Most aircraft will want to customize this behavior, but the default is
- reasonable for any aircraft with numbered radios.
- """
-
- @staticmethod
- def channel_name(radio_id: int, channel_id: int) -> str:
- """Returns the name of the channel for the given radio and channel."""
- return f"COMM{radio_id} Ch {channel_id}"
-
-
-class SingleRadioChannelNamer(ChannelNamer):
- """Channel namer for the aircraft with only a single radio.
-
- Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
- it's not necessary for us to name the radio when naming the channel.
- """
-
- @staticmethod
- def channel_name(radio_id: int, channel_id: int) -> str:
- return f"Ch {channel_id}"
-
-
-class HueyChannelNamer(ChannelNamer):
- """Channel namer for the UH-1H."""
-
- @staticmethod
- def channel_name(radio_id: int, channel_id: int) -> str:
- return f"COM3 Ch {channel_id}"
-
-
-class MirageChannelNamer(ChannelNamer):
- """Channel namer for the M-2000."""
-
- @staticmethod
- def channel_name(radio_id: int, channel_id: int) -> str:
- radio_name = ["V/UHF", "UHF"][radio_id - 1]
- return f"{radio_name} Ch {channel_id}"
-
-
-class TomcatChannelNamer(ChannelNamer):
- """Channel namer for the F-14."""
-
- @staticmethod
- def channel_name(radio_id: int, channel_id: int) -> str:
- radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
- return f"{radio_name} Ch {channel_id}"
-
-
-class ViggenChannelNamer(ChannelNamer):
- """Channel namer for the AJS37."""
-
- @staticmethod
- def channel_name(radio_id: int, channel_id: int) -> str:
- if channel_id >= 4:
- channel_letter = "EFGH"[channel_id - 4]
- return f"FR 24 {channel_letter}"
- return f"FR 22 Special {channel_id}"
-
-
-class ViperChannelNamer(ChannelNamer):
- """Channel namer for the F-16."""
-
- @staticmethod
- def channel_name(radio_id: int, channel_id: int) -> str:
- return f"COM{radio_id} Ch {channel_id}"
-
-
-class SCR522ChannelNamer(ChannelNamer):
- """
- Channel namer for P-51 & P-47D
- """
-
- @staticmethod
- def channel_name(radio_id: int, channel_id: int) -> str:
- if channel_id > 3:
- return "?"
- else:
- return f"Button " + "ABCD"[channel_id - 1]
-
-
@dataclass(frozen=True)
class ChannelAssignment:
radio_id: int
@@ -276,9 +138,6 @@ class FlightData:
#: The package that the flight belongs to.
package: Package
- #: The country that the flight belongs to.
- country: str
-
flight_type: FlightType
#: All units in the flight.
@@ -319,7 +178,7 @@ class FlightData:
def __init__(
self,
package: Package,
- country: str,
+ aircraft_type: AircraftType,
flight_type: FlightType,
units: List[FlyingUnit],
size: int,
@@ -335,7 +194,7 @@ class FlightData:
custom_name: Optional[str],
) -> None:
self.package = package
- self.country = country
+ self.aircraft_type = aircraft_type
self.flight_type = flight_type
self.units = units
self.size = size
@@ -357,11 +216,6 @@ class FlightData:
"""List of playable units in the flight."""
return [u for u in self.units if u.is_human()]
- @property
- def aircraft_type(self) -> FlyingType:
- """Returns the type of aircraft in this flight."""
- return self.units[0].unit_type
-
def num_radio_channels(self, radio_id: int) -> int:
"""Returns the number of preset channels for the given radio."""
# Note: pydcs only initializes the radio presets for client slots.
@@ -387,302 +241,6 @@ class FlightData:
)
-class RadioChannelAllocator:
- """Base class for radio channel allocators."""
-
- def assign_channels_for_flight(
- self, flight: FlightData, air_support: AirSupport
- ) -> None:
- """Assigns mission frequencies to preset channels for the flight."""
- raise NotImplementedError
-
-
-@dataclass(frozen=True)
-class CommonRadioChannelAllocator(RadioChannelAllocator):
- """Radio channel allocator suitable for most aircraft.
-
- Most of the aircraft with preset channels available have one or more radios
- with 20 or more channels available (typically per-radio, but this is not the
- case for the JF-17).
- """
-
- #: Index of the radio used for intra-flight communications. Matches the
- #: index of the panel_radio field of the pydcs.dcs.planes object.
- inter_flight_radio_index: Optional[int]
-
- #: Index of the radio used for intra-flight communications. Matches the
- #: index of the panel_radio field of the pydcs.dcs.planes object.
- intra_flight_radio_index: Optional[int]
-
- def assign_channels_for_flight(
- self, flight: FlightData, air_support: AirSupport
- ) -> None:
- if self.intra_flight_radio_index is not None:
- flight.assign_channel(
- self.intra_flight_radio_index, 1, flight.intra_flight_channel
- )
-
- if self.inter_flight_radio_index is None:
- return
-
- # For cases where the inter-flight and intra-flight radios share presets
- # (the JF-17 only has one set of channels, even though it can use two
- # channels simultaneously), start assigning inter-flight channels at 2.
- radio_id = self.inter_flight_radio_index
- if self.intra_flight_radio_index == radio_id:
- first_channel = 2
- else:
- first_channel = 1
-
- last_channel = flight.num_radio_channels(radio_id)
- channel_alloc = iter(range(first_channel, last_channel + 1))
-
- if flight.departure.atc is not None:
- flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
-
- # TODO: If there ever are multiple AWACS, limit to mission relevant.
- for awacs in air_support.awacs:
- flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
-
- if flight.arrival != flight.departure and flight.arrival.atc is not None:
- flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
-
- try:
- # TODO: Skip incompatible tankers.
- for tanker in air_support.tankers:
- flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
-
- if flight.divert is not None and flight.divert.atc is not None:
- flight.assign_channel(radio_id, next(channel_alloc), flight.divert.atc)
- except StopIteration:
- # Any remaining channels are nice-to-haves, but not necessary for
- # the few aircraft with a small number of channels available.
- pass
-
-
-@dataclass(frozen=True)
-class NoOpChannelAllocator(RadioChannelAllocator):
- """Channel allocator for aircraft that don't support preset channels."""
-
- def assign_channels_for_flight(
- self, flight: FlightData, air_support: AirSupport
- ) -> None:
- pass
-
-
-@dataclass(frozen=True)
-class FarmerRadioChannelAllocator(RadioChannelAllocator):
- """Preset channel allocator for the MiG-19P."""
-
- def assign_channels_for_flight(
- self, flight: FlightData, air_support: AirSupport
- ) -> None:
- # The Farmer only has 6 preset channels. It also only has a VHF radio,
- # and currently our ATC data and AWACS are only in the UHF band.
- radio_id = 1
- flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
- # TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
- # TODO: Assign 2 and 3 to AWACS if it is VHF.
-
-
-@dataclass(frozen=True)
-class ViggenRadioChannelAllocator(RadioChannelAllocator):
- """Preset channel allocator for the AJS37."""
-
- def assign_channels_for_flight(
- self, flight: FlightData, air_support: AirSupport
- ) -> None:
- # The Viggen's preset channels are handled differently from other
- # aircraft. The aircraft automatically configures channels for every
- # allied flight in the game (including AWACS) and for every airfield. As
- # such, we don't need to allocate any of those. There are seven presets
- # we can modify, however: three channels for the main radio intended for
- # communication with wingmen, and four emergency channels for the backup
- # radio. We'll set the first channel of the main radio to the
- # intra-flight channel, and the first three emergency channels to each
- # of the flight plan's airfields. The fourth emergency channel is always
- # the guard channel.
- radio_id = 1
- flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
- if flight.departure.atc is not None:
- flight.assign_channel(radio_id, 4, flight.departure.atc)
- if flight.arrival.atc is not None:
- flight.assign_channel(radio_id, 5, flight.arrival.atc)
- # TODO: Assign divert to 6 when we support divert airfields.
-
-
-@dataclass(frozen=True)
-class SCR522RadioChannelAllocator(RadioChannelAllocator):
- """Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
-
- def assign_channels_for_flight(
- self, flight: FlightData, air_support: AirSupport
- ) -> None:
- radio_id = 1
- flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
- if flight.departure.atc is not None:
- flight.assign_channel(radio_id, 2, flight.departure.atc)
- if flight.arrival.atc is not None:
- flight.assign_channel(radio_id, 3, flight.arrival.atc)
-
- # TODO : Some GCI on Channel 4 ?
-
-
-@dataclass(frozen=True)
-class AircraftData:
- """Additional aircraft data not exposed by pydcs."""
-
- #: The type of radio used for inter-flight communications.
- inter_flight_radio: Radio
-
- #: The type of radio used for intra-flight communications.
- intra_flight_radio: Radio
-
- #: The radio preset channel allocator, if the aircraft supports channel
- #: presets. If the aircraft does not support preset channels, this will be
- #: None.
- channel_allocator: Optional[RadioChannelAllocator]
-
- #: Defines how channels should be named when printed in the kneeboard.
- channel_namer: Type[ChannelNamer] = ChannelNamer
-
-
-# Indexed by the id field of the pydcs PlaneType.
-AIRCRAFT_DATA: Dict[str, AircraftData] = {
- "A-10C": AircraftData(
- inter_flight_radio=get_radio("AN/ARC-164"),
- # VHF for intraflight is not accepted anymore by DCS
- # (see https://forums.eagle.ru/showthread.php?p=4499738).
- intra_flight_radio=get_radio("AN/ARC-164"),
- channel_allocator=NoOpChannelAllocator(),
- ),
- "AJS37": AircraftData(
- # The AJS37 has somewhat unique radio configuration. Two backup radio
- # (FR 24) can only operate simultaneously with the main radio in guard
- # mode. As such, we only use the main radio for both inter- and intra-
- # flight communication.
- inter_flight_radio=get_radio("FR 22"),
- intra_flight_radio=get_radio("FR 22"),
- channel_allocator=ViggenRadioChannelAllocator(),
- channel_namer=ViggenChannelNamer,
- ),
- "AV8BNA": AircraftData(
- inter_flight_radio=get_radio("AN/ARC-210"),
- intra_flight_radio=get_radio("AN/ARC-210"),
- channel_allocator=CommonRadioChannelAllocator(
- inter_flight_radio_index=2, intra_flight_radio_index=1
- ),
- ),
- "F-14B": AircraftData(
- inter_flight_radio=get_radio("AN/ARC-159"),
- intra_flight_radio=get_radio("AN/ARC-182"),
- channel_allocator=CommonRadioChannelAllocator(
- inter_flight_radio_index=1, intra_flight_radio_index=2
- ),
- channel_namer=TomcatChannelNamer,
- ),
- "F-16C_50": AircraftData(
- inter_flight_radio=get_radio("AN/ARC-164"),
- intra_flight_radio=get_radio("AN/ARC-222"),
- # COM2 is the AN/ARC-222, which is the VHF radio we want to use for
- # intra-flight communication to leave COM1 open for UHF inter-flight.
- channel_allocator=CommonRadioChannelAllocator(
- inter_flight_radio_index=1, intra_flight_radio_index=2
- ),
- channel_namer=ViperChannelNamer,
- ),
- "FA-18C_hornet": AircraftData(
- inter_flight_radio=get_radio("AN/ARC-210"),
- intra_flight_radio=get_radio("AN/ARC-210"),
- # DCS will clobber channel 1 of the first radio compatible with the
- # flight's assigned frequency. Since the F/A-18's two radios are both
- # AN/ARC-210s, radio 1 will be compatible regardless of which frequency
- # is assigned, so we must use radio 1 for the intra-flight radio.
- channel_allocator=CommonRadioChannelAllocator(
- inter_flight_radio_index=2, intra_flight_radio_index=1
- ),
- ),
- "JF-17": AircraftData(
- inter_flight_radio=get_radio("R&S M3AR UHF"),
- intra_flight_radio=get_radio("R&S M3AR VHF"),
- channel_allocator=CommonRadioChannelAllocator(
- inter_flight_radio_index=1, intra_flight_radio_index=1
- ),
- # Same naming pattern as the Viper, so just reuse that.
- channel_namer=ViperChannelNamer,
- ),
- "Ka-50": AircraftData(
- inter_flight_radio=get_radio("R-800L1"),
- intra_flight_radio=get_radio("R-800L1"),
- # The R-800L1 doesn't have preset channels, and the other radio is for
- # communications with FAC and ground units, which don't currently have
- # radios assigned, so no channels to configure.
- channel_allocator=NoOpChannelAllocator(),
- ),
- "M-2000C": AircraftData(
- inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"),
- intra_flight_radio=get_radio("TRT ERA 7200 UHF"),
- channel_allocator=CommonRadioChannelAllocator(
- inter_flight_radio_index=1, intra_flight_radio_index=2
- ),
- channel_namer=MirageChannelNamer,
- ),
- "MiG-15bis": AircraftData(
- inter_flight_radio=get_radio("RSI-6K HF"),
- intra_flight_radio=get_radio("RSI-6K HF"),
- channel_allocator=NoOpChannelAllocator(),
- ),
- "MiG-19P": AircraftData(
- inter_flight_radio=get_radio("RSIU-4V"),
- intra_flight_radio=get_radio("RSIU-4V"),
- channel_allocator=FarmerRadioChannelAllocator(),
- channel_namer=SingleRadioChannelNamer,
- ),
- "MiG-21Bis": AircraftData(
- inter_flight_radio=get_radio("RSIU-5V"),
- intra_flight_radio=get_radio("RSIU-5V"),
- channel_allocator=CommonRadioChannelAllocator(
- inter_flight_radio_index=1, intra_flight_radio_index=1
- ),
- channel_namer=SingleRadioChannelNamer,
- ),
- "P-51D": AircraftData(
- inter_flight_radio=get_radio("SCR522"),
- intra_flight_radio=get_radio("SCR522"),
- channel_allocator=CommonRadioChannelAllocator(
- inter_flight_radio_index=1, intra_flight_radio_index=1
- ),
- channel_namer=SCR522ChannelNamer,
- ),
- "UH-1H": AircraftData(
- inter_flight_radio=get_radio("AN/ARC-51BX"),
- # Ideally this would use the AN/ARC-131 because that radio is supposed
- # to be used for flight comms, but DCS won't allow it as the flight's
- # frequency, nor will it allow the AN/ARC-134.
- intra_flight_radio=get_radio("AN/ARC-51BX"),
- channel_allocator=CommonRadioChannelAllocator(
- inter_flight_radio_index=1, intra_flight_radio_index=1
- ),
- channel_namer=HueyChannelNamer,
- ),
- "F-22A": AircraftData(
- inter_flight_radio=get_radio("SCR-522"),
- intra_flight_radio=get_radio("SCR-522"),
- channel_allocator=None,
- channel_namer=SCR522ChannelNamer,
- ),
- "JAS39Gripen": AircraftData(
- inter_flight_radio=get_radio("R&S Series 6000"),
- intra_flight_radio=get_radio("R&S Series 6000"),
- channel_allocator=None,
- ),
-}
-AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
-AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
-AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
-AIRCRAFT_DATA["JAS39Gripen_AG"] = AIRCRAFT_DATA["JAS39Gripen"]
-
-
class AircraftConflictGenerator:
def __init__(
self,
@@ -718,21 +276,6 @@ class AircraftConflictGenerator:
total += flight.client_count
return total
- def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
- """Allocates an intra-flight channel to a group.
-
- Args:
- airframe: The type of aircraft a channel should be allocated for.
-
- Returns:
- The frequency of the intra-flight channel.
- """
- try:
- aircraft_data = AIRCRAFT_DATA[airframe.id]
- return self.radio_registry.alloc_for_radio(aircraft_data.intra_flight_radio)
- except KeyError:
- return get_fallback_channel(airframe)
-
@staticmethod
def _start_type(start_type: str) -> StartType:
if start_type == "Runway":
@@ -838,7 +381,7 @@ class AircraftConflictGenerator:
):
channel = self.radio_registry.alloc_uhf()
else:
- channel = self.get_intra_flight_channel(unit_type)
+ channel = flight.unit_type.alloc_flight_radio(self.radio_registry)
group.set_frequency(channel.mhz)
divert = None
@@ -848,7 +391,7 @@ class AircraftConflictGenerator:
self.flights.append(
FlightData(
package=package,
- country=self.game.faction_for(player=flight.departure.captured).country,
+ aircraft_type=flight.unit_type,
flight_type=flight.flight_type,
units=group.units,
size=len(group.units),
@@ -894,12 +437,11 @@ class AircraftConflictGenerator:
callsign = callsign_for_support_unit(group)
tacan = self.tacan_registy.alloc_for_band(TacanBand.Y)
- variant = db.unit_type_name(flight.flight_plan.flight.unit_type)
self.air_support.tankers.append(
TankerInfo(
group_name=str(group.name),
callsign=callsign,
- variant=variant,
+ variant=flight.unit_type.name,
freq=channel,
tacan=tacan,
start_time=flight.flight_plan.patrol_start_time,
@@ -958,7 +500,7 @@ class AircraftConflictGenerator:
group = self.m.flight_group(
country=side,
name=name,
- aircraft_type=flight.unit_type,
+ aircraft_type=flight.unit_type.dcs_unit_type,
airport=None,
position=pos,
altitude=alt.meters,
@@ -1092,7 +634,7 @@ class AircraftConflictGenerator:
control_point: Airfield,
country: Country,
faction: Faction,
- aircraft: Type[FlyingType],
+ aircraft: AircraftType,
number: int,
) -> None:
for _ in range(number):
@@ -1114,7 +656,7 @@ class AircraftConflictGenerator:
group = self._generate_at_airport(
name=namegen.next_aircraft_name(country, control_point.id, flight),
side=country,
- unit_type=aircraft,
+ unit_type=aircraft.dcs_unit_type,
count=1,
start_type="Cold",
airport=control_point.airport,
@@ -1188,7 +730,7 @@ class AircraftConflictGenerator:
group = self._generate_at_group(
name=name,
side=country,
- unit_type=flight.unit_type,
+ unit_type=flight.unit_type.dcs_unit_type,
count=flight.count,
start_type=flight.start_type,
at=self.m.find_group(group_name),
@@ -1201,7 +743,7 @@ class AircraftConflictGenerator:
group = self._generate_at_airport(
name=name,
side=country,
- unit_type=flight.unit_type,
+ unit_type=flight.unit_type.dcs_unit_type,
count=flight.count,
start_type=flight.start_type,
airport=cp.airport,
@@ -1243,7 +785,7 @@ class AircraftConflictGenerator:
if flight.client_count > 0:
return True
- return flight.unit_type in GUN_RELIANT_AIRFRAMES
+ return flight.unit_type.always_keeps_gun
def configure_behavior(
self,
@@ -1283,7 +825,7 @@ class AircraftConflictGenerator:
@staticmethod
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
if hasattr(flight.unit_type, "eplrs"):
- if flight.unit_type.eplrs:
+ if flight.unit_type.dcs_unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
def configure_cap(
diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py
index c3f97791..23ff09a3 100644
--- a/gen/airsupportgen.py
+++ b/gen/airsupportgen.py
@@ -103,13 +103,13 @@ class AirSupportConflictGenerator:
)
if not self.game.settings.disable_legacy_tanker:
-
fallback_tanker_number = 0
for i, tanker_unit_type in enumerate(
self.game.faction_for(player=True).tankers
):
- alt, airspeed = self._get_tanker_params(tanker_unit_type)
+ # TODO: Make loiter altitude a property of the unit type.
+ alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
variant = db.unit_type_name(tanker_unit_type)
freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
diff --git a/gen/armor.py b/gen/armor.py
index 3b9f93ee..dd16f94f 100644
--- a/gen/armor.py
+++ b/gen/armor.py
@@ -28,6 +28,7 @@ from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup
from dcs.unittype import VehicleType
from game import db
+from game.dcs.aircrafttype import AircraftType
from game.unitmap import UnitMap
from game.utils import heading_sum, opposite_heading
from game.theater.controlpoint import ControlPoint
@@ -174,14 +175,14 @@ class GroundConflictGenerator:
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
code = 1688 - len(self.jtacs)
- utype = MQ_9_Reaper
- if self.game.player_faction.jtac_unit is not None:
- utype = self.game.player_faction.jtac_unit
+ utype = self.game.player_faction.jtac_unit
+ if self.game.player_faction.jtac_unit is None:
+ utype = AircraftType.named("MQ-9 Reaper")
jtac = self.mission.flight_group(
country=self.mission.country(self.game.player_country),
name=n,
- aircraft_type=utype,
+ aircraft_type=utype.dcs_unit_type,
position=position[0],
airport=None,
altitude=5000,
diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py
index 2ba8cfec..7dfe061b 100644
--- a/gen/flights/ai_flight_planner.py
+++ b/gen/flights/ai_flight_planner.py
@@ -17,14 +17,10 @@ from typing import (
Set,
TYPE_CHECKING,
Tuple,
- Type,
TypeVar,
- Union,
)
-from dcs.unittype import FlyingType
-
-from game.factions.faction import Faction
+from game.dcs.aircrafttype import AircraftType
from game.infos.information import Information
from game.procurement import AircraftProcurementRequest
from game.profiling import logged_duration, MultiEventTracer
@@ -256,7 +252,7 @@ class PackageBuilder:
return True
def find_divert_field(
- self, aircraft: Type[FlyingType], arrival: ControlPoint
+ self, aircraft: AircraftType, arrival: ControlPoint
) -> Optional[ControlPoint]:
divert_limit = nautical_miles(150)
for airfield in self.closest_airfields.operational_airfields_within(
@@ -867,7 +863,7 @@ class CoalitionMissionPlanner:
for cp in self.objective_finder.friendly_control_points():
inventory = self.game.aircraft_inventory.for_control_point(cp)
for aircraft, available in inventory.all_aircraft:
- self.message("Unused aircraft", f"{available} {aircraft.id} from {cp}")
+ self.message("Unused aircraft", f"{available} {aircraft} from {cp}")
def plan_flight(
self,
diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py
index 36486434..8ce78d13 100644
--- a/gen/flights/ai_flight_planner_db.py
+++ b/gen/flights/ai_flight_planner_db.py
@@ -104,6 +104,7 @@ from dcs.planes import (
)
from dcs.unittype import FlyingType
+from game.dcs.aircrafttype import AircraftType
from gen.flights.flight import FlightType
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A
@@ -415,7 +416,7 @@ REFUELING_CAPABALE = [
]
-def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
+def dcs_types_for_task(task: FlightType) -> list[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
if task in cap_missions:
return CAP_CAPABLE
@@ -450,7 +451,15 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return []
-def tasks_for_aircraft(aircraft: Type[FlyingType]) -> list[FlightType]:
+def aircraft_for_task(task: FlightType) -> list[AircraftType]:
+ dcs_types = dcs_types_for_task(task)
+ types: list[AircraftType] = []
+ for dcs_type in dcs_types:
+ types.extend(AircraftType.for_dcs_type(dcs_type))
+ return types
+
+
+def tasks_for_aircraft(aircraft: AircraftType) -> list[FlightType]:
tasks = []
for task in FlightType:
if aircraft in aircraft_for_task(task):
diff --git a/gen/flights/flight.py b/gen/flights/flight.py
index bbdb6cd0..cf5c011f 100644
--- a/gen/flights/flight.py
+++ b/gen/flights/flight.py
@@ -1,16 +1,15 @@
from __future__ import annotations
-from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
-from typing import List, Optional, TYPE_CHECKING, Type, Union
+from typing import List, Optional, TYPE_CHECKING, Union
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
from dcs.unit import Unit
-from dcs.unittype import FlyingType
from game import db
+from game.dcs.aircrafttype import AircraftType
from game.squadrons import Pilot, Squadron
from game.theater.controlpoint import ControlPoint, MissionTarget
from game.utils import Distance, meters
@@ -300,7 +299,7 @@ class Flight:
return self.roster.player_count
@property
- def unit_type(self) -> Type[FlyingType]:
+ def unit_type(self) -> AircraftType:
return self.squadron.aircraft
@property
@@ -325,13 +324,11 @@ class Flight:
self.roster.clear()
def __repr__(self):
- name = db.unit_type_name(self.unit_type)
if self.custom_name:
- return f"{self.custom_name} {self.count} x {name}"
- return f"[{self.flight_type}] {self.count} x {name}"
+ return f"{self.custom_name} {self.count} x {self.unit_type}"
+ return f"[{self.flight_type}] {self.count} x {self.unit_type}"
def __str__(self):
- name = db.unit_get_expanded_info(self.country, self.unit_type, "name")
if self.custom_name:
- return f"{self.custom_name} {self.count} x {name}"
- return f"[{self.flight_type}] {self.count} x {name}"
+ return f"{self.custom_name} {self.count} x {self.unit_type}"
+ return f"[{self.flight_type}] {self.count} x {self.unit_type}"
diff --git a/gen/flights/loadouts.py b/gen/flights/loadouts.py
index 845f7751..0a51245a 100644
--- a/gen/flights/loadouts.py
+++ b/gen/flights/loadouts.py
@@ -1,11 +1,10 @@
from __future__ import annotations
import datetime
-from typing import Optional, List, Iterator, Type, TYPE_CHECKING, Mapping
-
-from dcs.unittype import FlyingType
+from typing import Optional, List, Iterator, TYPE_CHECKING, Mapping
from game.data.weapons import Weapon, Pylon
+from game.dcs.aircrafttype import AircraftType
if TYPE_CHECKING:
from gen.flights.flight import Flight
@@ -27,9 +26,7 @@ class Loadout:
def derive_custom(self, name: str) -> Loadout:
return Loadout(name, self.pylons, self.date, is_custom=True)
- def degrade_for_date(
- self, unit_type: Type[FlyingType], date: datetime.date
- ) -> Loadout:
+ def degrade_for_date(self, unit_type: AircraftType, date: datetime.date) -> Loadout:
if self.date is not None and self.date <= date:
return Loadout(self.name, self.pylons, self.date)
@@ -61,7 +58,7 @@ class Loadout:
# {"CLSID": class ID, "num": pylon number}
# "tasks": List (as a dict) of task IDs the payload is used by.
# }
- payloads = flight.unit_type.load_payloads()
+ payloads = flight.unit_type.dcs_unit_type.load_payloads()
for payload in payloads.values():
name = payload["name"]
pylons = payload["pylons"]
@@ -126,8 +123,8 @@ class Loadout:
for name in cls.default_loadout_names_for(flight):
# This operation is cached, but must be called before load_by_name will
# work.
- flight.unit_type.load_payloads()
- payload = flight.unit_type.loadout_by_name(name)
+ flight.unit_type.dcs_unit_type.load_payloads()
+ payload = flight.unit_type.dcs_unit_type.loadout_by_name(name)
if payload is not None:
return Loadout(
name,
diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py
index 6b787f95..25e28ef9 100644
--- a/gen/flights/traveltime.py
+++ b/gen/flights/traveltime.py
@@ -25,16 +25,13 @@ if TYPE_CHECKING:
class GroundSpeed:
@classmethod
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
- if not issubclass(flight.unit_type, FlyingType):
- raise TypeError("Flight has non-flying unit")
-
# TODO: Expose both a cruise speed and target speed.
# The cruise speed can be used for ascent, hold, join, and RTB to save
# on fuel, but mission speed will be fast enough to keep the flight
# safer.
# DCS's max speed is in kph at 0 MSL.
- max_speed = kph(flight.unit_type.max_speed)
+ max_speed = flight.unit_type.max_speed
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
# Aircraft is supersonic. Limit to mach 0.85 to conserve fuel and
# account for heavily loaded jets.
diff --git a/gen/kneeboard.py b/gen/kneeboard.py
index 71544a26..62fd9d25 100644
--- a/gen/kneeboard.py
+++ b/gen/kneeboard.py
@@ -32,15 +32,15 @@ from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Iterator
from PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission
from dcs.unit import Unit
-from dcs.unittype import FlyingType
from tabulate import tabulate
from game.data.alic import AlicCodes
from game.db import unit_type_from_name
+from game.dcs.aircrafttype import AircraftType
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
from game.theater.bullseye import Bullseye
from game.utils import meters
-from .aircraft import AIRCRAFT_DATA, FlightData
+from .aircraft import FlightData
from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from .flights.flight import FlightWaypoint, FlightWaypointType, FlightType
@@ -142,7 +142,8 @@ class KneeboardPage:
"""Writes the kneeboard page to the given path."""
raise NotImplementedError
- def format_ll(self, ll: LatLon) -> str:
+ @staticmethod
+ def format_ll(ll: LatLon) -> str:
ns = "N" if ll.latitude >= 0 else "S"
ew = "E" if ll.longitude >= 0 else "W"
return f"{ll.latitude:.4}°{ns} {ll.longitude:.4}°{ew}"
@@ -355,8 +356,9 @@ class BriefingPage(KneeboardPage):
if channel is None:
return str(frequency)
- namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
- channel_name = namer.channel_name(channel.radio_id, channel.channel)
+ channel_name = self.flight.aircraft_type.channel_name(
+ channel.radio_id, channel.channel
+ )
return f"{channel_name}\n{frequency}"
@@ -452,9 +454,10 @@ class SupportPage(KneeboardPage):
if channel is None:
return str(frequency)
- namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
- channel_name = namer.channel_name(channel.radio_id, channel.channel)
- return f"{channel_name} {frequency}"
+ channel_name = self.flight.aircraft_type.channel_name(
+ channel.radio_id, channel.channel
+ )
+ return f"{channel_name}\n{frequency}"
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
if time is None:
@@ -565,14 +568,14 @@ class KneeboardGenerator(MissionInfoGenerator):
temp_dir = Path("kneeboards")
temp_dir.mkdir(exist_ok=True)
for aircraft, pages in self.pages_by_airframe().items():
- aircraft_dir = temp_dir / aircraft.id
+ aircraft_dir = temp_dir / aircraft.dcs_unit_type.id
aircraft_dir.mkdir(exist_ok=True)
for idx, page in enumerate(pages):
page_path = aircraft_dir / f"page{idx:02}.png"
page.write(page_path)
- self.mission.add_aircraft_kneeboard(aircraft, page_path)
+ self.mission.add_aircraft_kneeboard(aircraft.dcs_unit_type, page_path)
- def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]:
+ def pages_by_airframe(self) -> Dict[AircraftType, List[KneeboardPage]]:
"""Returns a list of kneeboard pages per airframe in the mission.
Only client flights will be included, but because DCS does not support
@@ -583,7 +586,7 @@ class KneeboardGenerator(MissionInfoGenerator):
A dict mapping aircraft types to the list of kneeboard pages for
that aircraft.
"""
- all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list)
+ all_flights: Dict[AircraftType, List[KneeboardPage]] = defaultdict(list)
for flight in self.flights:
if not flight.client_units:
continue
diff --git a/gen/naming.py b/gen/naming.py
index dad364dc..f4964cd2 100644
--- a/gen/naming.py
+++ b/gen/naming.py
@@ -6,6 +6,7 @@ from dcs.country import Country
from dcs.unittype import UnitType
from game import db
+from game.dcs.aircrafttype import AircraftType
from gen.flights.flight import Flight
@@ -290,7 +291,7 @@ class NameGenerator:
country.id,
cls.aircraft_number,
parent_base_id,
- db.unit_type_name(flight.unit_type),
+ flight.unit_type.name,
)
@classmethod
@@ -318,11 +319,9 @@ class NameGenerator:
return "awacs|{}|{}|0|".format(country.id, cls.number)
@classmethod
- def next_tanker_name(cls, country: Country, unit_type: UnitType):
+ def next_tanker_name(cls, country: Country, unit_type: AircraftType):
cls.number += 1
- return "tanker|{}|{}|0|{}".format(
- country.id, cls.number, db.unit_type_name(unit_type)
- )
+ return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.name)
@classmethod
def next_carrier_name(cls, country: Country):
diff --git a/gen/radios.py b/gen/radios.py
index 333647df..22968397 100644
--- a/gen/radios.py
+++ b/gen/radios.py
@@ -153,7 +153,7 @@ def get_radio(name: str) -> Radio:
for radio in RADIOS:
if radio.name == name:
return radio
- raise KeyError
+ raise KeyError(f"Unknown radio: {name}")
class RadioRegistry:
diff --git a/qt_ui/models.py b/qt_ui/models.py
index 0bf5920f..4c4a52fe 100644
--- a/qt_ui/models.py
+++ b/qt_ui/models.py
@@ -143,7 +143,7 @@ class PackageModel(QAbstractListModel):
@staticmethod
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
"""Returns the icon that should be displayed for the flight."""
- name = db.unit_type_name(flight.unit_type)
+ name = flight.unit_type.dcs_id
if name in AIRCRAFT_ICONS:
return QIcon(AIRCRAFT_ICONS[name])
return None
@@ -402,7 +402,7 @@ class AirWingModel(QAbstractListModel):
@staticmethod
def icon_for_squadron(squadron: Squadron) -> Optional[QIcon]:
"""Returns the icon that should be displayed for the squadron."""
- name = db.unit_type_name(squadron.aircraft)
+ name = squadron.aircraft.dcs_id
if name in AIRCRAFT_ICONS:
return QIcon(AIRCRAFT_ICONS[name])
return None
diff --git a/qt_ui/widgets/combos/QAircraftTypeSelector.py b/qt_ui/widgets/combos/QAircraftTypeSelector.py
index b7949aa2..80dfa5b0 100644
--- a/qt_ui/widgets/combos/QAircraftTypeSelector.py
+++ b/qt_ui/widgets/combos/QAircraftTypeSelector.py
@@ -30,10 +30,7 @@ class QAircraftTypeSelector(QComboBox):
self.clear()
for aircraft in aircraft_types:
if aircraft in aircraft_for_task(mission_type):
- self.addItem(
- f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
- userData=aircraft,
- )
+ self.addItem(f"{aircraft}", userData=aircraft)
current_aircraft_index = self.findData(current_aircraft)
if current_aircraft_index != -1:
self.setCurrentIndex(current_aircraft_index)
diff --git a/qt_ui/windows/AirWingDialog.py b/qt_ui/windows/AirWingDialog.py
index 80c6443a..525f3c88 100644
--- a/qt_ui/windows/AirWingDialog.py
+++ b/qt_ui/windows/AirWingDialog.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
-from typing import Optional, Type, Iterator
+from typing import Optional, Iterator
from PySide2.QtCore import (
QItemSelectionModel,
@@ -20,9 +20,7 @@ from PySide2.QtWidgets import (
QTableWidgetItem,
QWidget,
)
-from dcs.unittype import FlyingType
-from game import db
from game.inventory import ControlPointAircraftInventory
from game.squadrons import Squadron
from gen.flights.flight import Flight
@@ -45,9 +43,7 @@ class SquadronDelegate(TwoColumnRowDelegate):
return self.air_wing_model.data(index, Qt.DisplayRole)
elif (row, column) == (0, 1):
squadron = self.air_wing_model.data(index, AirWingModel.SquadronRole)
- return db.unit_get_expanded_info(
- squadron.country, squadron.aircraft, "name"
- )
+ return squadron.aircraft.name
elif (row, column) == (1, 0):
return self.squadron(index).nickname
elif (row, column) == (1, 1):
@@ -111,7 +107,6 @@ class AircraftInventoryData:
@classmethod
def from_flight(cls, flight: Flight) -> Iterator[AircraftInventoryData]:
- unit_type_name = cls.format_unit_type(flight.unit_type, flight.country)
num_units = flight.count
flight_type = flight.flight_type.value
target = flight.package.target.name
@@ -125,7 +120,7 @@ class AircraftInventoryData:
player = "Player" if pilot.player else "AI"
yield AircraftInventoryData(
flight.departure.name,
- unit_type_name,
+ flight.unit_type.name,
flight_type,
target,
pilot_name,
@@ -134,24 +129,19 @@ class AircraftInventoryData:
@classmethod
def each_from_inventory(
- cls, inventory: ControlPointAircraftInventory, country: str
+ cls, inventory: ControlPointAircraftInventory
) -> Iterator[AircraftInventoryData]:
for unit_type, num_units in inventory.all_aircraft:
- unit_type_name = cls.format_unit_type(unit_type, country)
for _ in range(0, num_units):
yield AircraftInventoryData(
inventory.control_point.name,
- unit_type_name,
+ unit_type.name,
"Idle",
"N/A",
"N/A",
"N/A",
)
- @staticmethod
- def format_unit_type(aircraft: Type[FlyingType], country: str) -> str:
- return db.unit_get_expanded_info(country, aircraft, "name")
-
class AirInventoryView(QWidget):
def __init__(self, game_model: GameModel) -> None:
@@ -201,9 +191,7 @@ class AirInventoryView(QWidget):
game = self.game_model.game
for control_point, inventory in game.aircraft_inventory.inventories.items():
if control_point.captured:
- yield from AircraftInventoryData.each_from_inventory(
- inventory, game.country_for(player=True)
- )
+ yield from AircraftInventoryData.each_from_inventory(inventory)
def get_data(self, only_unallocated: bool) -> Iterator[AircraftInventoryData]:
yield from self.iter_unallocated_aircraft()
diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py
index 86e59e0d..f1635c3d 100644
--- a/qt_ui/windows/QDebriefingWindow.py
+++ b/qt_ui/windows/QDebriefingWindow.py
@@ -22,15 +22,7 @@ class LossGrid(QGridLayout):
def __init__(self, debriefing: Debriefing, player: bool) -> None:
super().__init__()
- if player:
- country = debriefing.player_country
- else:
- country = debriefing.enemy_country
-
- self.add_loss_rows(
- debriefing.air_losses.by_type(player),
- lambda u: db.unit_get_expanded_info(country, u, "name"),
- )
+ self.add_loss_rows(debriefing.air_losses.by_type(player), lambda u: u.name)
self.add_loss_rows(
debriefing.front_line_losses_by_type(player),
lambda u: db.unit_type_name(u),
diff --git a/qt_ui/windows/QUnitInfoWindow.py b/qt_ui/windows/QUnitInfoWindow.py
index aaadd06d..0438b952 100644
--- a/qt_ui/windows/QUnitInfoWindow.py
+++ b/qt_ui/windows/QUnitInfoWindow.py
@@ -1,41 +1,40 @@
-import logging
-from typing import Type
+from typing import Type, Union
-from PySide2 import QtCore
+import dcs
from PySide2.QtCore import Qt
-from PySide2.QtGui import QIcon, QMovie, QPixmap
+from PySide2.QtGui import QIcon
from PySide2.QtWidgets import (
QDialog,
QGridLayout,
- QGroupBox,
- QHBoxLayout,
QLabel,
- QMessageBox,
- QPushButton,
QTextBrowser,
QFrame,
)
-from jinja2 import Environment, FileSystemLoader, select_autoescape
-from dcs.unittype import UnitType, FlyingType, VehicleType
-import dcs
-from qt_ui.uiconstants import AIRCRAFT_BANNERS, VEHICLE_BANNERS
-
-from game.game import Game
-from game import db
+from dcs.unittype import UnitType
import gen.flights.ai_flight_planner_db
+from game import db
+from game.dcs.aircrafttype import AircraftType
+from game.game import Game
from gen.flights.flight import FlightType
+from qt_ui.uiconstants import AIRCRAFT_BANNERS, VEHICLE_BANNERS
class QUnitInfoWindow(QDialog):
- def __init__(self, game: Game, unit_type: Type[UnitType]) -> None:
- super(QUnitInfoWindow, self).__init__()
+ def __init__(
+ self, game: Game, unit_type: Union[AircraftType, Type[UnitType]]
+ ) -> None:
+ super().__init__()
self.setModal(True)
self.game = game
self.unit_type = unit_type
- self.setWindowTitle(
- f"Unit Info: {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'name')}"
- )
+ if isinstance(unit_type, AircraftType):
+ self.name = unit_type.name
+ else:
+ self.name = db.unit_get_expanded_info(
+ self.game.player_country, self.unit_type, "name"
+ )
+ self.setWindowTitle(f"Unit Info: {self.name}")
self.setWindowIcon(QIcon("./resources/icon.png"))
self.setMinimumHeight(570)
self.setMaximumWidth(640)
@@ -71,7 +70,7 @@ class QUnitInfoWindow(QDialog):
self.details_grid_layout.setMargin(0)
self.name_box = QLabel(
- f"Name: {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'manufacturer')} {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'name')}"
+ f"Name: {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'manufacturer')} {self.name}"
)
self.name_box.setProperty("style", "info-element")
diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py
index a9209521..3973015a 100644
--- a/qt_ui/windows/basemenu/QRecruitBehaviour.py
+++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py
@@ -1,5 +1,5 @@
import logging
-from typing import Type
+from typing import Type, Union
from PySide2.QtWidgets import (
QGroupBox,
@@ -10,9 +10,9 @@ from PySide2.QtWidgets import (
QSizePolicy,
QSpacerItem,
)
-from dcs.unittype import UnitType
+from dcs.unittype import VehicleType
-from game import db
+from game.dcs.aircrafttype import AircraftType
from game.theater import ControlPoint
from game.unitdelivery import PendingUnitDeliveries
from qt_ui.models import GameModel
@@ -47,7 +47,7 @@ class QRecruitBehaviour:
def add_purchase_row(
self,
- unit_type: Type[UnitType],
+ unit_type: Union[AircraftType, Type[VehicleType]],
layout: QLayout,
row: int,
) -> int:
@@ -61,13 +61,7 @@ class QRecruitBehaviour:
existing_units = self.cp.base.total_units_of_type(unit_type)
scheduled_units = self.pending_deliveries.units.get(unit_type, 0)
- unitName = QLabel(
- ""
- + db.unit_get_expanded_info(
- self.game_model.game.player_country, unit_type, "name"
- )
- + ""
- )
+ unitName = QLabel(f"{self.name_of(unit_type)}")
unitName.setSizePolicy(
QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
)
@@ -81,7 +75,7 @@ class QRecruitBehaviour:
self.existing_units_labels[unit_type] = existing_units
self.bought_amount_labels[unit_type] = amount_bought
- price = QLabel("$ {:02d} m".format(db.PRICES[unit_type]))
+ price = QLabel(f"$ {self.price_of(unit_type)} M")
price.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
buysell = QGroupBox()
@@ -155,7 +149,7 @@ class QRecruitBehaviour:
return row + 1
- def _update_count_label(self, unit_type: Type[UnitType]):
+ def _update_count_label(self, unit_type: Union[AircraftType, Type[VehicleType]]):
self.bought_amount_labels[unit_type].setText(
"{}".format(
@@ -172,32 +166,31 @@ class QRecruitBehaviour:
def update_available_budget(self) -> None:
GameUpdateSignal.get_instance().updateBudget(self.game_model.game)
- def buy(self, unit_type: Type[UnitType]):
+ def buy(self, unit_type: Union[AircraftType, Type[VehicleType]]):
if not self.enable_purchase(unit_type):
logging.error(f"Purchase of {unit_type.id} not allowed at {self.cp.name}")
return
- price = db.PRICES[unit_type]
self.pending_deliveries.order({unit_type: 1})
- self.budget -= price
+ self.budget -= self.price_of(unit_type)
self._update_count_label(unit_type)
self.update_available_budget()
def sell(self, unit_type):
if self.pending_deliveries.available_next_turn(unit_type) > 0:
- price = db.PRICES[unit_type]
- self.budget += price
+ self.budget += self.price_of(unit_type)
self.pending_deliveries.sell({unit_type: 1})
if self.pending_deliveries.units[unit_type] == 0:
del self.pending_deliveries.units[unit_type]
self._update_count_label(unit_type)
self.update_available_budget()
- def enable_purchase(self, unit_type: Type[UnitType]) -> bool:
- price = db.PRICES[unit_type]
- return self.budget >= price
+ def enable_purchase(
+ self, unit_type: Union[AircraftType, Type[VehicleType]]
+ ) -> bool:
+ return self.budget >= self.price_of(unit_type)
- def enable_sale(self, unit_type: Type[UnitType]) -> bool:
+ def enable_sale(self, unit_type: Union[AircraftType, Type[VehicleType]]) -> bool:
return True
def info(self, unit_type):
@@ -209,3 +202,9 @@ class QRecruitBehaviour:
Set the maximum number of units that can be bought
"""
self.maximum_units = maximum_units
+
+ def name_of(self, unit_type: Union[AircraftType, Type[VehicleType]]) -> str:
+ raise NotImplementedError
+
+ def price_of(self, unit_type: Union[AircraftType, Type[VehicleType]]) -> int:
+ raise NotImplementedError
diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
index b5a6e7fe..4e12a131 100644
--- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
+++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
@@ -1,5 +1,5 @@
import logging
-from typing import Set, Type
+from typing import Set
from PySide2.QtCore import Qt
from PySide2.QtWidgets import (
@@ -13,9 +13,8 @@ from PySide2.QtWidgets import (
QWidget,
)
from dcs.helicopters import helicopter_map
-from dcs.unittype import FlyingType, UnitType
-from game import db
+from game.dcs.aircrafttype import AircraftType
from game.theater import ControlPoint, ControlPointType
from qt_ui.models import GameModel
from qt_ui.uiconstants import ICONS
@@ -48,13 +47,11 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
task_box_layout = QGridLayout()
row = 0
- unit_types: Set[Type[FlyingType]] = set()
+ unit_types: Set[AircraftType] = set()
for unit_type in self.game_model.game.player_faction.aircrafts:
- if not issubclass(unit_type, FlyingType):
- raise RuntimeError(f"Non-flying aircraft found in faction: {unit_type}")
- if self.cp.is_carrier and unit_type not in db.CARRIER_CAPABLE:
+ if self.cp.is_carrier and not unit_type.carrier_capable:
continue
- if self.cp.is_lha and unit_type not in db.LHA_CAPABLE:
+ if self.cp.is_lha and not unit_type.lha_capable:
continue
if (
self.cp.cptype in [ControlPointType.FOB, ControlPointType.FARP]
@@ -65,9 +62,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
sorted_units = sorted(
unit_types,
- key=lambda u: db.unit_get_expanded_info(
- self.game_model.game.player_country, u, "name"
- ),
+ key=lambda u: u.name,
)
for unit_type in sorted_units:
row = self.add_purchase_row(unit_type, task_box_layout, row)
@@ -85,30 +80,33 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
main_layout.addWidget(scroll)
self.setLayout(main_layout)
- def enable_purchase(self, unit_type: Type[UnitType]) -> bool:
+ def enable_purchase(self, unit_type: AircraftType) -> bool:
if not super().enable_purchase(unit_type):
return False
- if not issubclass(unit_type, FlyingType):
- return False
if not self.cp.can_operate(unit_type):
return False
return True
- def enable_sale(self, unit_type: Type[UnitType]) -> bool:
- if not issubclass(unit_type, FlyingType):
- return False
+ def enable_sale(self, unit_type: AircraftType) -> bool:
if not self.cp.can_operate(unit_type):
return False
return True
- def buy(self, unit_type):
+ def name_of(self, unit_type: AircraftType) -> str:
+ return unit_type.name
+
+ def price_of(self, unit_type: AircraftType) -> int:
+ return unit_type.price
+
+ def buy(self, unit_type: AircraftType) -> None:
if self.maximum_units > 0:
if self.cp.unclaimed_parking(self.game_model.game) <= 0:
logging.debug(f"No space for additional aircraft at {self.cp}.")
QMessageBox.warning(
self,
"No space for additional aircraft",
- f"There is no parking space left at {self.cp.name} to accommodate another plane.",
+ f"There is no parking space left at {self.cp.name} to accommodate "
+ "another plane.",
QMessageBox.Ok,
)
return
@@ -122,7 +120,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
super().buy(unit_type)
self.hangar_status.update_label()
- def sell(self, unit_type: UnitType):
+ def sell(self, unit_type: AircraftType) -> None:
# Don't need to remove aircraft from the inventory if we're canceling
# orders.
if self.pending_deliveries.units.get(unit_type, 0) <= 0:
@@ -134,7 +132,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
QMessageBox.critical(
self,
"Could not sell aircraft",
- f"Attempted to sell one {unit_type.id} at {self.cp.name} "
+ f"Attempted to sell one {unit_type} at {self.cp.name} "
"but none are available. Are all aircraft currently "
"assigned to a mission?",
QMessageBox.Ok,
diff --git a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py
index 3f104e7d..63c80b6b 100644
--- a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py
+++ b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py
@@ -8,9 +8,10 @@ from PySide2.QtWidgets import (
QVBoxLayout,
QWidget,
)
-from dcs.unittype import UnitType
+from dcs.unittype import UnitType, VehicleType
from game import db
+from game.db import PRICES
from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
@@ -65,3 +66,11 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
def enable_sale(self, unit_type: Type[UnitType]) -> bool:
return self.pending_deliveries.pending_orders(unit_type) > 0
+
+ def name_of(self, unit_type: Type[VehicleType]) -> str:
+ return db.unit_get_expanded_info(
+ self.game_model.game.player_country, unit_type, "name"
+ )
+
+ def price_of(self, unit_type: Type[VehicleType]) -> int:
+ return PRICES[unit_type]
diff --git a/qt_ui/windows/basemenu/intel/QIntelInfo.py b/qt_ui/windows/basemenu/intel/QIntelInfo.py
index cc9c3ca6..2f30d169 100644
--- a/qt_ui/windows/basemenu/intel/QIntelInfo.py
+++ b/qt_ui/windows/basemenu/intel/QIntelInfo.py
@@ -28,10 +28,8 @@ class QIntelInfo(QFrame):
units_by_task: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
for unit_type, count in self.cp.base.aircraft.items():
if count:
- name = db.unit_get_expanded_info(
- self.game.enemy_country, unit_type, "name"
- )
- units_by_task[unit_type.task_default.name][name] += count
+ task_type = unit_type.dcs_unit_type.task_default.name
+ units_by_task[task_type][unit_type.name] += count
units_by_task = {
task: units_by_task[task] for task in sorted(units_by_task.keys())
diff --git a/qt_ui/windows/intel.py b/qt_ui/windows/intel.py
index 74203adf..6dd02915 100644
--- a/qt_ui/windows/intel.py
+++ b/qt_ui/windows/intel.py
@@ -84,10 +84,7 @@ class AircraftIntelLayout(IntelTableLayout):
for airframe, count in base.aircraft.items():
if not count:
continue
- self.add_row(
- db.unit_get_expanded_info(game.enemy_country, airframe, "name"),
- count,
- )
+ self.add_row(airframe.name, count)
self.add_spacer()
self.add_row("Total", total)
diff --git a/qt_ui/windows/mission/QFlightItem.py b/qt_ui/windows/mission/QFlightItem.py
index d8d9cb44..42f1d47f 100644
--- a/qt_ui/windows/mission/QFlightItem.py
+++ b/qt_ui/windows/mission/QFlightItem.py
@@ -1,6 +1,5 @@
from PySide2.QtGui import QStandardItem, QIcon
-from game import db
from gen.ato import Package
from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator
@@ -14,11 +13,8 @@ class QFlightItem(QStandardItem):
self.package = package
self.flight = flight
- if (
- db.unit_type_name(self.flight.unit_type).replace("/", " ")
- in AIRCRAFT_ICONS.keys()
- ):
- icon = QIcon((AIRCRAFT_ICONS[db.unit_type_name(self.flight.unit_type)]))
+ if self.flight.unit_type.dcs_id in AIRCRAFT_ICONS:
+ icon = QIcon((AIRCRAFT_ICONS[self.flight.unit_type.dcs_id]))
self.setIcon(icon)
self.setEditable(False)
estimator = TotEstimator(self.package)
diff --git a/qt_ui/windows/mission/flight/settings/QFlightTypeTaskInfo.py b/qt_ui/windows/mission/flight/settings/QFlightTypeTaskInfo.py
index c05a0fa6..32324676 100644
--- a/qt_ui/windows/mission/flight/settings/QFlightTypeTaskInfo.py
+++ b/qt_ui/windows/mission/flight/settings/QFlightTypeTaskInfo.py
@@ -1,6 +1,5 @@
-from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout
+from PySide2.QtWidgets import QLabel, QGroupBox, QGridLayout
-from game import db
from qt_ui.uiconstants import AIRCRAFT_ICONS
@@ -12,10 +11,8 @@ class QFlightTypeTaskInfo(QGroupBox):
layout = QGridLayout()
self.aircraft_icon = QLabel()
- if db.unit_type_name(self.flight.unit_type) in AIRCRAFT_ICONS:
- self.aircraft_icon.setPixmap(
- AIRCRAFT_ICONS[db.unit_type_name(self.flight.unit_type)]
- )
+ if self.flight.unit_type.dcs_id in AIRCRAFT_ICONS:
+ self.aircraft_icon.setPixmap(AIRCRAFT_ICONS[self.flight.unit_type.dcs_id])
self.task = QLabel("Task:")
self.task_type = QLabel(str(flight.flight_type))
diff --git a/resources/tools/convert_unit_data.py b/resources/tools/convert_unit_data.py
new file mode 100644
index 00000000..8aae5779
--- /dev/null
+++ b/resources/tools/convert_unit_data.py
@@ -0,0 +1,512 @@
+from __future__ import annotations
+
+import json
+from collections import defaultdict
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+from typing import Optional, Type
+
+import dcs
+import yaml
+from dcs.helicopters import (
+ AH_1W,
+ AH_64A,
+ AH_64D,
+ Ka_50,
+ Mi_24V,
+ Mi_28N,
+ Mi_8MT,
+ OH_58D,
+ SA342L,
+ SA342M,
+ SA342Minigun,
+ SA342Mistral,
+ SH_60B,
+ UH_1H,
+ UH_60A,
+ helicopter_map,
+)
+
+from dcs.planes import (
+ AV8BNA,
+ A_10A,
+ A_10C,
+ A_10C_2,
+ A_20G,
+ Bf_109K_4,
+ E_2C,
+ FA_18C_hornet,
+ FW_190A8,
+ FW_190D9,
+ F_14A_135_GR,
+ F_14B,
+ F_86F_Sabre,
+ Ju_88A4,
+ MiG_15bis,
+ MiG_19P,
+ P_47D_30,
+ P_47D_30bl1,
+ P_47D_40,
+ P_51D,
+ P_51D_30_NA,
+ S_3B,
+ S_3B_Tanker,
+ SpitfireLFMkIX,
+ SpitfireLFMkIXCW,
+ Su_25,
+ Su_25T,
+ Su_33,
+ plane_map,
+)
+from dcs.unittype import FlyingType
+
+from game.db import PRICES
+from game.factions.faction import unit_loader
+from game.radio.channels import (
+ RadioChannelAllocator,
+ ChannelNamer,
+ NoOpChannelAllocator,
+ ViggenRadioChannelAllocator,
+ ViggenChannelNamer,
+ CommonRadioChannelAllocator,
+ TomcatChannelNamer,
+ ViperChannelNamer,
+ MirageChannelNamer,
+ FarmerRadioChannelAllocator,
+ SingleRadioChannelNamer,
+ SCR522ChannelNamer,
+ HueyChannelNamer,
+)
+from gen.radios import get_radio, Radio
+from pydcs_extensions.a4ec.a4ec import A_4E_C
+from pydcs_extensions.mod_units import MODDED_AIRPLANES
+
+THIS_DIR = Path(__file__).resolve().parent
+SRC_ROOT = THIS_DIR.parent.parent
+UNIT_DATA_DIR = SRC_ROOT / "resources/units"
+FACTIONS_DIR = SRC_ROOT / "resources/factions"
+
+
+# List of airframes that rely on their gun as a primary weapon. We confiscate bullets
+# from most AI air-to-ground missions since they aren't smart enough to RTB when they're
+# out of everything other than bullets (DCS does not have an all-but-gun winchester
+# option) and we don't want to be attacking fully functional Tors with a Vulcan.
+#
+# These airframes are the exceptions. They probably should be using their gun regardless
+# of the mission type.
+GUN_RELIANT_AIRFRAMES: list[Type[FlyingType]] = [
+ AH_1W,
+ AH_64A,
+ AH_64D,
+ A_10A,
+ A_10C,
+ A_10C_2,
+ A_20G,
+ Bf_109K_4,
+ FW_190A8,
+ FW_190D9,
+ F_86F_Sabre,
+ Ju_88A4,
+ Ka_50,
+ MiG_15bis,
+ MiG_19P,
+ Mi_24V,
+ Mi_28N,
+ P_47D_30,
+ P_47D_30bl1,
+ P_47D_40,
+ P_51D,
+ P_51D_30_NA,
+ SpitfireLFMkIX,
+ SpitfireLFMkIXCW,
+ Su_25,
+ Su_25T,
+]
+
+CARRIER_CAPABLE = [
+ FA_18C_hornet,
+ F_14A_135_GR,
+ F_14B,
+ AV8BNA,
+ Su_33,
+ A_4E_C,
+ S_3B,
+ S_3B_Tanker,
+ E_2C,
+ UH_1H,
+ Mi_8MT,
+ Ka_50,
+ AH_1W,
+ OH_58D,
+ UH_60A,
+ SH_60B,
+ SA342L,
+ SA342M,
+ SA342Minigun,
+ SA342Mistral,
+]
+
+LHA_CAPABLE = [
+ AV8BNA,
+ UH_1H,
+ Mi_8MT,
+ Ka_50,
+ AH_1W,
+ OH_58D,
+ UH_60A,
+ SH_60B,
+ SA342L,
+ SA342M,
+ SA342Minigun,
+ SA342Mistral,
+]
+
+
+@dataclass(frozen=True)
+class AircraftData:
+ """Additional aircraft data not exposed by pydcs."""
+
+ #: The type of radio used for inter-flight communications.
+ inter_flight_radio: Radio
+
+ #: The type of radio used for intra-flight communications.
+ intra_flight_radio: Radio
+
+ #: The radio preset channel allocator, if the aircraft supports channel
+ #: presets. If the aircraft does not support preset channels, this will be
+ #: None.
+ channel_allocator: Optional[RadioChannelAllocator]
+
+ #: Defines how channels should be named when printed in the kneeboard.
+ channel_namer: Type[ChannelNamer] = ChannelNamer
+
+
+# Indexed by the id field of the pydcs PlaneType.
+AIRCRAFT_DATA: dict[str, AircraftData] = {
+ "A-10C": AircraftData(
+ inter_flight_radio=get_radio("AN/ARC-164"),
+ # VHF for intraflight is not accepted anymore by DCS
+ # (see https://forums.eagle.ru/showthread.php?p=4499738).
+ intra_flight_radio=get_radio("AN/ARC-164"),
+ channel_allocator=NoOpChannelAllocator(),
+ ),
+ "AJS37": AircraftData(
+ # The AJS37 has somewhat unique radio configuration. Two backup radio
+ # (FR 24) can only operate simultaneously with the main radio in guard
+ # mode. As such, we only use the main radio for both inter- and intra-
+ # flight communication.
+ inter_flight_radio=get_radio("FR 22"),
+ intra_flight_radio=get_radio("FR 22"),
+ channel_allocator=ViggenRadioChannelAllocator(),
+ channel_namer=ViggenChannelNamer,
+ ),
+ "AV8BNA": AircraftData(
+ inter_flight_radio=get_radio("AN/ARC-210"),
+ intra_flight_radio=get_radio("AN/ARC-210"),
+ channel_allocator=CommonRadioChannelAllocator(
+ inter_flight_radio_index=2, intra_flight_radio_index=1
+ ),
+ ),
+ "F-14B": AircraftData(
+ inter_flight_radio=get_radio("AN/ARC-159"),
+ intra_flight_radio=get_radio("AN/ARC-182"),
+ channel_allocator=CommonRadioChannelAllocator(
+ inter_flight_radio_index=1, intra_flight_radio_index=2
+ ),
+ channel_namer=TomcatChannelNamer,
+ ),
+ "F-16C_50": AircraftData(
+ inter_flight_radio=get_radio("AN/ARC-164"),
+ intra_flight_radio=get_radio("AN/ARC-222"),
+ # COM2 is the AN/ARC-222, which is the VHF radio we want to use for
+ # intra-flight communication to leave COM1 open for UHF inter-flight.
+ channel_allocator=CommonRadioChannelAllocator(
+ inter_flight_radio_index=1, intra_flight_radio_index=2
+ ),
+ channel_namer=ViperChannelNamer,
+ ),
+ "JF-17": AircraftData(
+ inter_flight_radio=get_radio("R&S M3AR UHF"),
+ intra_flight_radio=get_radio("R&S M3AR VHF"),
+ channel_allocator=CommonRadioChannelAllocator(
+ inter_flight_radio_index=1, intra_flight_radio_index=1
+ ),
+ # Same naming pattern as the Viper, so just reuse that.
+ channel_namer=ViperChannelNamer,
+ ),
+ "Ka-50": AircraftData(
+ inter_flight_radio=get_radio("R-800L1"),
+ intra_flight_radio=get_radio("R-800L1"),
+ # The R-800L1 doesn't have preset channels, and the other radio is for
+ # communications with FAC and ground units, which don't currently have
+ # radios assigned, so no channels to configure.
+ channel_allocator=NoOpChannelAllocator(),
+ ),
+ "M-2000C": AircraftData(
+ inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"),
+ intra_flight_radio=get_radio("TRT ERA 7200 UHF"),
+ channel_allocator=CommonRadioChannelAllocator(
+ inter_flight_radio_index=1, intra_flight_radio_index=2
+ ),
+ channel_namer=MirageChannelNamer,
+ ),
+ "MiG-15bis": AircraftData(
+ inter_flight_radio=get_radio("RSI-6K HF"),
+ intra_flight_radio=get_radio("RSI-6K HF"),
+ channel_allocator=NoOpChannelAllocator(),
+ ),
+ "MiG-19P": AircraftData(
+ inter_flight_radio=get_radio("RSIU-4V"),
+ intra_flight_radio=get_radio("RSIU-4V"),
+ channel_allocator=FarmerRadioChannelAllocator(),
+ channel_namer=SingleRadioChannelNamer,
+ ),
+ "MiG-21Bis": AircraftData(
+ inter_flight_radio=get_radio("RSIU-5V"),
+ intra_flight_radio=get_radio("RSIU-5V"),
+ channel_allocator=CommonRadioChannelAllocator(
+ inter_flight_radio_index=1, intra_flight_radio_index=1
+ ),
+ channel_namer=SingleRadioChannelNamer,
+ ),
+ "P-51D": AircraftData(
+ inter_flight_radio=get_radio("SCR522"),
+ intra_flight_radio=get_radio("SCR522"),
+ channel_allocator=CommonRadioChannelAllocator(
+ inter_flight_radio_index=1, intra_flight_radio_index=1
+ ),
+ channel_namer=SCR522ChannelNamer,
+ ),
+ "UH-1H": AircraftData(
+ inter_flight_radio=get_radio("AN/ARC-51BX"),
+ # Ideally this would use the AN/ARC-131 because that radio is supposed
+ # to be used for flight comms, but DCS won't allow it as the flight's
+ # frequency, nor will it allow the AN/ARC-134.
+ intra_flight_radio=get_radio("AN/ARC-51BX"),
+ channel_allocator=CommonRadioChannelAllocator(
+ inter_flight_radio_index=1, intra_flight_radio_index=1
+ ),
+ channel_namer=HueyChannelNamer,
+ ),
+ "F-22A": AircraftData(
+ inter_flight_radio=get_radio("SCR-522"),
+ intra_flight_radio=get_radio("SCR-522"),
+ channel_allocator=None,
+ channel_namer=SCR522ChannelNamer,
+ ),
+ "JAS39Gripen": AircraftData(
+ inter_flight_radio=get_radio("R&S Series 6000"),
+ intra_flight_radio=get_radio("R&S Series 6000"),
+ channel_allocator=None,
+ ),
+}
+AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
+AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
+AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
+AIRCRAFT_DATA["JAS39Gripen_AG"] = AIRCRAFT_DATA["JAS39Gripen"]
+
+
+class Converter:
+ def __init__(self) -> None:
+ self.all_variants: set[str] = set()
+ self.variant_map: dict[str, dict[str, str]] = {}
+ self.unconverted: set[Type[FlyingType]] = set(
+ k for k in PRICES if issubclass(k, FlyingType)
+ )
+
+ @staticmethod
+ def find_unit_id_for_faction_name(name: str) -> str:
+ unit_type = unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES])
+ if unit_type is None:
+ raise KeyError(f"Found no unit named {name}")
+ return unit_type.id
+
+ def convert(self) -> None:
+ data_path = UNIT_DATA_DIR / "unit_info_text.json"
+ with data_path.open(encoding="utf-8") as unit_data_file:
+ unit_data = json.load(unit_data_file)
+
+ for unit_name, data in dict(unit_data).items():
+ if self.convert_unit(unit_name, data):
+ unit_data.pop(unit_name)
+
+ with data_path.open("w", encoding="utf-8") as unit_data_file:
+ json.dump(unit_data, unit_data_file, indent=2)
+
+ for unconverted in self.unconverted:
+ self.generate_basic_info(unconverted)
+
+ for faction_path in FACTIONS_DIR.glob("*.json"):
+ self.update_faction(faction_path)
+
+ def update_faction(self, faction_path: Path) -> None:
+ with faction_path.open() as faction_file:
+ data = json.load(faction_file)
+
+ self.update_aircraft_list(data, "aircrafts")
+ self.update_aircraft_list(data, "awacs")
+ self.update_aircraft_list(data, "tankers")
+ self.update_aircraft_item(data, "jtac_unit")
+
+ if "liveries_overrides" in data:
+ new_liveries = {}
+ for aircraft, liveries in data["liveries_overrides"].items():
+ name = self.new_name_for(aircraft, data["country"])
+ new_liveries[name] = sorted(liveries)
+ data["liveries_overrides"] = new_liveries
+
+ with faction_path.open("w") as faction_file:
+ json.dump(data, faction_file, indent=2)
+
+ def new_name_for(self, old_name: str, country: str) -> str:
+ if old_name in self.all_variants:
+ return old_name
+ aircraft_id = self.find_unit_id_for_faction_name(old_name)
+ return self.variant_map[aircraft_id][country]
+
+ def update_aircraft_list(self, data: dict[str, Any], field: str) -> None:
+ if field not in data:
+ return
+
+ new_aircraft = []
+ for aircraft in data[field]:
+ new_aircraft.append(self.new_name_for(aircraft, data["country"]))
+ data[field] = sorted(new_aircraft)
+
+ def update_aircraft_item(self, data: dict[str, Any], field: str) -> None:
+ if field in data:
+ aircraft_name = data[field]
+ data[field] = self.new_name_for(aircraft_name, data["country"])
+
+ def generate_basic_info(self, unit_type: Type[FlyingType]) -> None:
+ self.all_variants.add(unit_type.id)
+ output_path = UNIT_DATA_DIR / "aircraft" / f"{unit_type.id}.yaml"
+ if output_path.exists():
+ # Already have data for this, don't clobber it, but do register the
+ # variant names.
+ with output_path.open() as unit_info_file:
+ data = yaml.safe_load(unit_info_file)
+ self.all_variants.update(data["variants"].keys())
+ return
+ with output_path.open("w") as output_file:
+ yaml.safe_dump(
+ {
+ "price": PRICES[unit_type],
+ "variants": {unit_type.id: None},
+ },
+ output_file,
+ )
+
+ self.variant_map[unit_type.id] = defaultdict(lambda: unit_type.id)
+
+ def convert_unit(
+ self, pydcs_name: str, data: list[dict[str, dict[str, str]]]
+ ) -> bool:
+ if len(data) != 1:
+ raise ValueError(f"Unexpected data format for {pydcs_name}")
+
+ unit_type: Type[FlyingType]
+ if pydcs_name in plane_map:
+ unit_type = plane_map[pydcs_name]
+ elif pydcs_name in helicopter_map:
+ unit_type = helicopter_map[pydcs_name]
+ else:
+ return False
+
+ self.unconverted.remove(unit_type)
+
+ variants_dict = data[0]
+ default = variants_dict.pop("default")
+
+ default_name = default["name"]
+ self.all_variants.add(default_name)
+ country_to_variant = defaultdict(lambda: default_name)
+
+ variants = {default_name: {}}
+ for country, variant_dict in variants_dict.items():
+ variant_name = variant_dict["name"]
+ self.all_variants.add(variant_name)
+ country_to_variant[country] = variant_name
+ variants[variant_name] = self.get_variant_data(variant_dict)
+
+ output_dict: dict[str, Any] = {"variants": variants, "price": PRICES[unit_type]}
+ output_dict.update(self.get_variant_data(default))
+
+ if unit_type in CARRIER_CAPABLE:
+ output_dict["carrier_capable"] = True
+ if unit_type in LHA_CAPABLE:
+ output_dict["lha_capable"] = True
+ if unit_type in GUN_RELIANT_AIRFRAMES:
+ output_dict["always_keeps_gun"] = True
+
+ try:
+ aircraft_data = AIRCRAFT_DATA[unit_type.id]
+ radio_dict: dict[str, Any] = {
+ "intra_flight": aircraft_data.intra_flight_radio.name,
+ "inter_flight": aircraft_data.inter_flight_radio.name,
+ }
+ channels_dict: dict[str, Any] = {}
+ if type(aircraft_data.channel_namer) != ChannelNamer:
+ channels_dict["namer"] = aircraft_data.channel_namer.name()
+ if aircraft_data.channel_allocator is not None:
+ alloc = aircraft_data.channel_allocator
+ if alloc.name() != "noop":
+ channels_dict["type"] = alloc.name()
+ if isinstance(alloc, CommonRadioChannelAllocator):
+ channels_dict[
+ "intra_flight_radio_index"
+ ] = alloc.intra_flight_radio_index
+ channels_dict[
+ "inter_flight_radio_index"
+ ] = alloc.inter_flight_radio_index
+ if channels_dict:
+ radio_dict["channels"] = channels_dict
+ except KeyError:
+ pass
+
+ output_path = UNIT_DATA_DIR / "aircraft" / f"{unit_type.id}.yaml"
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ with output_path.open("w") as output_file:
+ yaml.safe_dump(output_dict, output_file)
+
+ self.variant_map[pydcs_name] = country_to_variant
+ return True
+
+ @staticmethod
+ def get_variant_data(variant: dict[str, Any]) -> dict[str, Any]:
+ result = {}
+
+ try:
+ result["manufacturer"] = variant["manufacturer"]
+ except KeyError:
+ pass
+
+ try:
+ result["origin"] = variant["country-of-origin"]
+ except KeyError:
+ pass
+ try:
+ result["role"] = variant["role"]
+ except KeyError:
+ pass
+
+ try:
+ as_str = variant["year-of-variant-introduction"]
+ if as_str == "N/A":
+ result["introduced"] = None
+ else:
+ result["introduced"] = int(as_str)
+ except KeyError:
+ pass
+
+ try:
+ result["description"] = variant["text"]
+ except KeyError:
+ pass
+
+ return result
+
+
+if __name__ == "__main__":
+ Converter().convert()
diff --git a/resources/units/FA-18C_hornet.yaml b/resources/units/FA-18C_hornet.yaml
new file mode 100644
index 00000000..851d1837
--- /dev/null
+++ b/resources/units/FA-18C_hornet.yaml
@@ -0,0 +1,38 @@
+---
+variants:
+ - F/A-18C Hornet (Lot 20)
+ - CF-188 Hornet
+ - EF-18A+ Hornet
+price: 22
+carrier_capable: true
+radios:
+ intra_flight: AN/ARC-210
+ inter_flight: AN/ARC-210
+ channels:
+ type: common
+ # DCS will clobber channel 1 of the first radio compatible with the flight's
+ # assigned frequency. Since the F/A-18's two radios are both AN/ARC-210s,
+ # radio 1 will be compatible regardless of which frequency is assigned, so
+ # we must use radio 1 for the intra-flight radio.
+ intra_flight_radio_index: 1
+ inter_flight_radio_index: 2
+manufacturer: McDonnell Douglass
+role: Carrier-based Multirole Fighter
+origin: USA
+introduced: 1987
+description: >-
+ The F/A-18C Hornet is twin engine, supersonic fighter that is flown by a
+ single pilot in a "glass cockpit". It combines extreme maneuverability , a
+ deadly arsenal of weapons, and the ability to operate from an aircraft
+ carrier. Operated by several nations, this multi-role fighter has been
+ instrumental in conflicts from 1986 to today.
+
+ The Hornet is equipped with a large suite of sensors that includes a radar,
+ targeting pod, and a helmet mounted sight. In addition to its internal 20mm
+ cannon, the Hornet can be armed with a large assortment of unguided bombs and
+ rockets, laser and GPS-guided bombs, air-to-surface missiles of all sorts, and
+ both radar and infrared-guided air-to-air missiles.
+
+ The Hornet is also known for its extreme, slow-speed maneuverability in a
+ dogfight. Although incredibly deadly, the Hornet is also a very easy aircraft
+ to fly.