Configurable RF/TCN/ICLS/LINK4 with UI feedback

Resolves #70

Freq/Channel will turn orange when double booked.
Freq will turn red if GUARD freq was assigned.
This commit is contained in:
Raffson
2023-01-14 15:42:13 +01:00
parent ddb9d6968b
commit 88f984b0a8
37 changed files with 992 additions and 177 deletions

View File

@@ -12,6 +12,10 @@ from .flightroster import FlightRoster
from .flightstate import FlightState, Navigating, Uninitialized
from .flightstate.killed import Killed
from .loadouts import Loadout, Weapon
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
from ..radio.TacanContainer import TacanContainer
from ..radio.radios import RadioFrequency
from ..radio.tacan import TacanChannel
from ..sidc import (
Entity,
SidcDescribable,
@@ -36,7 +40,7 @@ if TYPE_CHECKING:
F18_TGP_PYLON: int = 4
class Flight(SidcDescribable):
class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
def __init__(
self,
package: Package,
@@ -49,6 +53,9 @@ class Flight(SidcDescribable):
custom_name: Optional[str] = None,
cargo: Optional[TransferOrder] = None,
roster: Optional[FlightRoster] = None,
frequency: Optional[RadioFrequency] = None,
channel: Optional[TacanChannel] = None,
callsign: Optional[str] = None,
) -> None:
self.id = uuid.uuid4()
self.package = package
@@ -68,6 +75,11 @@ class Flight(SidcDescribable):
self.custom_name = custom_name
self.group_id: int = 0
self.frequency = frequency
if self.unit_type.dcs_unit_type.tacan:
self.tacan = channel
self.tcn_name = callsign
# Only used by transport missions.
self.cargo = cargo

View File

@@ -13,13 +13,14 @@ from .flightplans.formation import FormationFlightPlan
from .flighttype import FlightType
from .packagewaypoints import PackageWaypoints
from .traveltime import TotEstimator
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
from ..radio.radios import RadioFrequency
if TYPE_CHECKING:
from game.theater import ControlPoint, MissionTarget
class Package:
class Package(RadioFrequencyContainer):
"""A mission package."""
def __init__(

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from game.ato.packagewaypoints import PackageWaypoints
from game.data.doctrine import MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
@@ -9,6 +9,11 @@ if TYPE_CHECKING:
from game import Game
def try_set_attr(obj: Any, attr_name: str, val: Any = None) -> None:
if not hasattr(obj, attr_name):
setattr(obj, attr_name, val)
class Migrator:
def __init__(self, game: Game):
self.game = game
@@ -18,6 +23,8 @@ class Migrator:
self._update_doctrine()
self._update_packagewaypoints()
self._update_package_attributes()
self._update_control_points()
self._update_flights()
def _update_doctrine(self) -> None:
doctrines = [
@@ -40,13 +47,32 @@ class Migrator:
def _update_packagewaypoints(self) -> None:
for c in self.game.coalitions:
for p in c.ato.packages:
if p.flights and not hasattr(p.waypoints, "initial"):
p.waypoints = PackageWaypoints.create(p, c)
if p.flights:
try_set_attr(p.waypoints, "initial", PackageWaypoints.create(p, c))
def _update_package_attributes(self) -> None:
for c in self.game.coalitions:
for p in c.ato.packages:
if not hasattr(p, "custom_name"):
p.custom_name = None
if not hasattr(p, "frequency"):
p.frequency = None
try_set_attr(p, "custom_name")
try_set_attr(p, "frequency")
def _update_control_points(self) -> None:
for cp in self.game.theater.controlpoints:
is_carrier = cp.is_carrier
is_lha = cp.is_lha
is_fob = cp.category == "fob"
radio_configurable = is_carrier or is_lha or is_fob
if radio_configurable:
try_set_attr(cp, "frequency")
if is_carrier or is_lha:
try_set_attr(cp, "tacan")
try_set_attr(cp, "tcn_name")
try_set_attr(cp, "icls_channel")
try_set_attr(cp, "icls_name")
try_set_attr(cp, "link4")
def _update_flights(self) -> None:
for f in self.game.db.flights.objects.values():
try_set_attr(f, "frequency")
try_set_attr(f, "tacan")
try_set_attr(f, "tcn_name")

View File

@@ -104,11 +104,7 @@ class AircraftGenerator:
ato: The ATO to spawn aircraft for.
dynamic_runways: Runway data for carriers and FARPs.
"""
for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)):
if package.frequency is None:
continue
if package.frequency not in self.radio_registry.allocated_channels:
self.radio_registry.reserve(package.frequency)
self._reserve_frequencies_and_tacan(ato)
for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)):
if not package.flights:
@@ -206,3 +202,18 @@ class AircraftGenerator:
).configure()
)
return group
def _reserve_frequencies_and_tacan(self, ato: AirTaskingOrder) -> None:
for package in ato.packages:
if package.frequency is None:
continue
if package.frequency not in self.radio_registry.allocated_channels:
self.radio_registry.reserve(package.frequency)
for f in package.flights:
if (
f.frequency
and f.frequency not in self.radio_registry.allocated_channels
):
self.radio_registry.reserve(f.frequency)
if f.tacan and f.tacan not in self.tacan_registy.allocated_channels:
self.tacan_registy.mark_unavailable(f.tacan)

View File

@@ -133,15 +133,16 @@ class FlightGroupConfigurator:
laser_codes.append(None)
def setup_radios(self) -> RadioFrequency:
if (freq := self.flight.package.frequency) is None:
freq = self.flight.frequency
if freq is None and (freq := self.flight.package.frequency) is None:
freq = self.radio_registry.alloc_uhf()
self.flight.package.frequency = freq
elif freq not in self.radio_registry.allocated_channels:
if freq not in self.radio_registry.allocated_channels:
self.radio_registry.reserve(freq)
if self.flight.flight_type in {FlightType.AEWC, FlightType.REFUELING}:
self.register_air_support(freq)
elif self.flight.client_count:
elif self.flight.frequency is None and self.flight.client_count:
freq = self.flight.unit_type.alloc_flight_radio(self.radio_registry)
self.group.set_frequency(freq.mhz)
@@ -162,7 +163,12 @@ class FlightGroupConfigurator:
)
)
elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan):
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
if self.flight.tacan is None:
tacan = self.tacan_registry.alloc_for_band(
TacanBand.Y, TacanUsage.AirToAir
)
else:
tacan = self.flight.tacan
self.mission_data.tankers.append(
TankerInfo(
group_name=str(self.group.name),

View File

@@ -71,11 +71,14 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
if self.flight.unit_type.dcs_unit_type.tacan:
tanker_info = self.mission_data.tankers[-1]
tacan = tanker_info.tacan
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(tanker_info.callsign)
if self.flight.tcn_name is None:
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(tanker_info.callsign)
else:
tacan_callsign = self.flight.tcn_name
waypoint.add_task(
ActivateBeaconCommand(

View File

@@ -37,6 +37,7 @@ from .missiondata import MissionData
from .tgogenerator import TgoGenerator
from .triggergenerator import TriggerGenerator
from .visualsgenerator import VisualsGenerator
from ..radio.TacanContainer import TacanContainer
if TYPE_CHECKING:
from game import Game
@@ -177,6 +178,9 @@ class MissionGenerator:
logging.warning(f"TACAN beacon has no channel: {beacon.callsign}")
else:
self.tacan_registry.mark_unavailable(beacon.tacan_channel)
for cp in self.game.theater.controlpoints:
if isinstance(cp, TacanContainer) and cp.tacan is not None:
self.tacan_registry.mark_unavailable(cp.tacan)
def initialize_radio_registry(
self, unique_map_frequencies: set[RadioFrequency]

View File

@@ -22,6 +22,8 @@ from dcs.ships import (
CVN_73,
CVN_75,
Stennis,
Forrestal,
LHA_Tarawa,
)
from dcs.statics import Fortification
from dcs.task import (
@@ -35,7 +37,7 @@ from dcs.task import (
)
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone
from dcs.unit import Unit, InvisibleFARP
from dcs.unit import Unit, InvisibleFARP, BaseFARP
from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import ShipType, VehicleType
from dcs.vehicles import vehicle_map
@@ -45,10 +47,16 @@ from game.missiongenerator.groundforcepainter import (
GroundForcePainter,
)
from game.missiongenerator.missiondata import CarrierInfo, MissionData
from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
from game.runways import RunwayData
from game.theater import ControlPoint, TheaterGroundObject, TheaterUnit
from game.theater import (
ControlPoint,
TheaterGroundObject,
TheaterUnit,
NavalControlPoint,
)
from game.theater.theatergroundobject import (
CarrierGroundObject,
GenericCarrierGroundObject,
@@ -351,7 +359,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
def __init__(
self,
ground_object: GenericCarrierGroundObject,
control_point: ControlPoint,
control_point: NavalControlPoint,
country: Country,
game: Game,
mission: Mission,
@@ -372,9 +380,12 @@ class GenericCarrierGenerator(GroundObjectGenerator):
self.mission_data = mission_data
def generate(self) -> None:
# This can also be refactored as the general generation was updated
atc = self.radio_registry.alloc_uhf()
if self.control_point.frequency is not None:
atc = self.control_point.frequency
if atc not in self.radio_registry.allocated_channels:
self.radio_registry.reserve(atc)
else:
atc = self.radio_registry.alloc_uhf()
for g_id, group in enumerate(self.ground_object.groups):
if not group.units:
@@ -409,15 +420,33 @@ class GenericCarrierGenerator(GroundObjectGenerator):
f"Error generating carrier group for {self.control_point.name}"
)
ship_group.units[0].type = carrier_type.id
tacan = self.tacan_registry.alloc_for_band(
TacanBand.X, TacanUsage.TransmitReceive
)
tacan_callsign = self.tacan_callsign()
icls = next(self.icls_alloc)
if self.control_point.tacan is None:
tacan = self.tacan_registry.alloc_for_band(
TacanBand.X, TacanUsage.TransmitReceive
)
else:
tacan = self.control_point.tacan
if self.control_point.tcn_name is None:
tacan_callsign = self.tacan_callsign()
else:
tacan_callsign = self.control_point.tcn_name
link4 = None
if carrier_type in [Stennis, CVN_71, CVN_72, CVN_73, CVN_75]:
link4 = self.radio_registry.alloc_uhf()
self.activate_beacons(ship_group, tacan, tacan_callsign, icls, link4)
link4carriers = [Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal]
if carrier_type in link4carriers:
if self.control_point.link4 is None:
link4 = self.radio_registry.alloc_uhf()
else:
link4 = self.control_point.link4
icls = None
icls_name = self.control_point.icls_name
if carrier_type in link4carriers or carrier_type == LHA_Tarawa:
if self.control_point.icls_channel is None:
icls = next(self.icls_alloc)
else:
icls = self.control_point.icls_channel
self.activate_beacons(
ship_group, tacan, tacan_callsign, icls, icls_name, link4
)
self.add_runway_data(
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
)
@@ -461,7 +490,8 @@ class GenericCarrierGenerator(GroundObjectGenerator):
group: ShipGroup,
tacan: TacanChannel,
callsign: str,
icls: int,
icls: Optional[int] = None,
icls_name: Optional[str] = None,
link4: Optional[RadioFrequency] = None,
) -> None:
group.points[0].tasks.append(
@@ -473,12 +503,14 @@ class GenericCarrierGenerator(GroundObjectGenerator):
aa=False,
)
)
group.points[0].tasks.append(
ActivateICLSCommand(icls, unit_id=group.units[0].id)
)
if icls is not None:
icls_name = "" if icls_name is None else icls_name
group.points[0].tasks.append(
ActivateICLSCommand(icls, group.units[0].id, icls_name)
)
if link4 is not None:
group.points[0].tasks.append(
ActivateLink4Command(int(link4.mhz), group.units[0].id)
ActivateLink4Command(link4.hertz, group.units[0].id)
)
group.points[0].tasks.append(ActivateACLSCommand(unit_id=group.units[0].id))
@@ -488,7 +520,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
atc: RadioFrequency,
tacan: TacanChannel,
callsign: str,
icls: int,
icls: Optional[int],
) -> None:
# TODO: Make unit name usable.
# This relies on one control point mapping exactly
@@ -590,6 +622,13 @@ class HelipadGenerator:
self.helipads.add_unit(
InvisibleFARP(self.m.terrain, self.m.next_unit_id(), name_i)
)
# Set FREQ
if isinstance(self.cp, RadioFrequencyContainer) and self.cp.frequency:
for hp in self.helipads.units:
if isinstance(hp, BaseFARP):
hp.heliport_frequency = self.cp.frequency.mhz
pad = self.helipads.units[-1]
pad.position = helipad
pad.heading = heading
@@ -661,7 +700,9 @@ class TgoGenerator:
for ground_object in cp.ground_objects:
generator: GroundObjectGenerator
if isinstance(ground_object, CarrierGroundObject):
if isinstance(ground_object, CarrierGroundObject) and isinstance(
cp, NavalControlPoint
):
generator = CarrierGenerator(
ground_object,
cp,
@@ -675,7 +716,9 @@ class TgoGenerator:
self.unit_map,
self.mission_data,
)
elif isinstance(ground_object, LhaGroundObject):
elif isinstance(ground_object, LhaGroundObject) and isinstance(
cp, NavalControlPoint
):
generator = LhaGenerator(
ground_object,
cp,

View File

@@ -0,0 +1,6 @@
from typing import Optional
class ICLSContainer:
icls_channel: Optional[int] = None
icls_name: Optional[str] = None

View File

@@ -0,0 +1,8 @@
from typing import Optional
from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
from game.radio.radios import RadioFrequency
class Link4Container(RadioFrequencyContainer):
link4: Optional[RadioFrequency] = None

View File

@@ -0,0 +1,7 @@
from typing import Optional
from game.radio.radios import RadioFrequency
class RadioFrequencyContainer:
frequency: Optional[RadioFrequency] = None

View File

@@ -0,0 +1,8 @@
from typing import Optional
from game.radio.tacan import TacanChannel
class TacanContainer:
tacan: Optional[TacanChannel] = None
tcn_name: Optional[str] = None

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import itertools
import logging
import random
import re
from dataclasses import dataclass
from typing import Dict, FrozenSet, Iterator, List, Set, Tuple
@@ -156,57 +157,61 @@ class ChannelInUseError(RuntimeError):
# TODO: Figure out appropriate steps for each radio. These are just guesses.
#: List of all known radios used by aircraft in the game.
RADIOS: List[Radio] = [
Radio("AN/ARC-164", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
Radio("AN/ARC-186(V) AM", (RadioRange(MHz(116), MHz(152), MHz(1), Modulation.AM),)),
Radio("AN/ARC-186(V) FM", (RadioRange(MHz(30), MHz(76), MHz(1), Modulation.FM),)),
Radio("AN/ARC-164", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
Radio(
"AN/ARC-186(V) AM", (RadioRange(MHz(116), MHz(152), kHz(25), Modulation.AM),)
),
Radio("AN/ARC-186(V) FM", (RadioRange(MHz(30), MHz(76), kHz(25), Modulation.FM),)),
Radio(
"AN/ARC-210",
(
RadioRange(
MHz(225),
MHz(400),
MHz(1),
kHz(25),
Modulation.AM,
frozenset((MHz(243),)),
),
RadioRange(MHz(136), MHz(155), MHz(1), Modulation.AM),
RadioRange(MHz(156), MHz(174), MHz(1), Modulation.FM),
RadioRange(MHz(118), MHz(136), MHz(1), Modulation.AM),
RadioRange(MHz(30), MHz(88), MHz(1), Modulation.FM),
RadioRange(MHz(136), MHz(155), kHz(25), Modulation.AM),
RadioRange(MHz(156), MHz(174), kHz(25), Modulation.FM),
RadioRange(MHz(118), MHz(136), kHz(25), Modulation.AM),
RadioRange(MHz(30), MHz(88), kHz(25), Modulation.FM),
# The AN/ARC-210 can also use 225-400 and 136-155 with FM Modulation
RadioRange(
MHz(225),
MHz(400),
MHz(1),
kHz(25),
Modulation.FM,
frozenset((MHz(243),)),
),
RadioRange(MHz(136), MHz(155), MHz(1), Modulation.FM),
RadioRange(MHz(136), MHz(155), kHz(25), Modulation.FM),
),
),
Radio("AN/ARC-222", (RadioRange(MHz(116), MHz(152), MHz(1), Modulation.AM),)),
Radio("SCR-522", (RadioRange(MHz(100), MHz(156), MHz(1), Modulation.AM),)),
Radio("A.R.I. 1063", (RadioRange(MHz(100), MHz(156), MHz(1), Modulation.AM),)),
Radio("AN/ARC-222", (RadioRange(MHz(116), MHz(152), kHz(25), Modulation.AM),)),
Radio("SCR-522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
Radio("A.R.I. 1063", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
Radio("BC-1206", (RadioRange(kHz(200), kHz(400), kHz(10), Modulation.AM),)),
Radio(
"TRT ERA 7000 V/UHF",
(
RadioRange(MHz(118), MHz(150), MHz(1), Modulation.AM),
RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),
RadioRange(MHz(118), MHz(150), kHz(25), Modulation.AM),
RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),
),
),
Radio("TRT ERA 7200 UHF", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
Radio(
"TRT ERA 7200 UHF", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)
),
# Tomcat radios
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
Radio("AN/ARC-159", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
Radio("AN/ARC-159", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
Radio(
"AN/ARC-182",
(
RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),
RadioRange(MHz(108), MHz(174), MHz(1), Modulation.AM),
RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),
RadioRange(MHz(108), MHz(174), kHz(25), Modulation.AM),
# The Range from 30-88MHz should be FM but its modeled as AM in dcs
RadioRange(MHz(30), MHz(88), MHz(1), Modulation.AM),
RadioRange(MHz(30), MHz(88), kHz(25), Modulation.AM),
),
),
Radio(
@@ -220,14 +225,14 @@ RADIOS: List[Radio] = [
# 4 preset channels (A/B/C/D)
Radio("SCR522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
# JF-17 Radios should use AM
Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), MHz(1), Modulation.AM),)),
Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), kHz(25), Modulation.AM),)),
Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
# MiG-15bis
Radio("RSI-6K HF", (RadioRange(MHz(3, 750), MHz(5), kHz(25), Modulation.AM),)),
# MiG-19P
Radio("RSIU-4V", (RadioRange(MHz(100), MHz(150), MHz(1), Modulation.AM),)),
Radio("RSIU-4V", (RadioRange(MHz(100), MHz(150), kHz(25), Modulation.AM),)),
# MiG-21bis
Radio("RSIU-5V", (RadioRange(MHz(118), MHz(140), MHz(1), Modulation.AM),)),
Radio("RSIU-5V", (RadioRange(MHz(118), MHz(140), kHz(25), Modulation.AM),)),
# Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
Radio("R-800L1", (RadioRange(MHz(220), MHz(400), kHz(25), Modulation.AM),)),
@@ -263,7 +268,7 @@ RADIOS: List[Radio] = [
RadioRange(
MHz(225),
MHz(400),
MHz(1),
kHz(25),
Modulation.AM,
frozenset((MHz(243),)),
),
@@ -275,28 +280,28 @@ RADIOS: List[Radio] = [
RadioRange(
MHz(30),
MHz(88),
MHz(1),
kHz(25),
Modulation.FM,
frozenset((MHz(40, 500),)),
),
RadioRange(
MHz(108),
MHz(156),
MHz(1),
kHz(25),
Modulation.AM,
frozenset((MHz(121, 500),)),
),
RadioRange(
MHz(156),
MHz(174),
MHz(1),
kHz(25),
Modulation.FM,
frozenset((MHz(156, 800),)),
),
RadioRange(
MHz(225),
MHz(400),
MHz(1),
kHz(25),
Modulation.AM, # Actually AM/FM, but we can't represent that.
frozenset((MHz(243),)),
),
@@ -323,6 +328,13 @@ def get_radio(name: str) -> Radio:
raise KeyError(f"Unknown radio: {name}")
def random_frequency(radio: Radio) -> RadioFrequency:
range: RadioRange = radio.ranges[0]
delta = round(random.random() * (range.maximum.hertz - range.minimum.hertz))
delta -= delta % range.step.hertz
return RadioFrequency(range.minimum.hertz + delta)
class RadioRegistry:
"""Manages allocation of radio channels.
@@ -368,9 +380,8 @@ class RadioRegistry:
OutOfChannelsError: All channels compatible with the given radio are
already allocated.
"""
allocator = self.radio_allocators[radio]
try:
while (channel := next(allocator)) in self.allocated_channels:
while (channel := random_frequency(radio)) in self.allocated_channels:
pass
self.reserve(channel)
return channel

View File

@@ -54,8 +54,8 @@ class TacanChannel:
if match is None:
raise ValueError(f"Could not parse TACAN from {text}")
number = int(match.group(1))
if not number:
raise ValueError("TACAN channel cannot be 0")
if not (0 < number <= 126):
raise ValueError("TACAN channel cannot be 0 or larger than 126")
return TacanChannel(number, TacanBand(match.group(2)))

View File

@@ -22,6 +22,7 @@ from typing import (
TYPE_CHECKING,
Tuple,
Type,
Union,
)
from uuid import UUID
@@ -73,6 +74,10 @@ from ..data.units import UnitClass
from ..db import Database
from ..dcs.aircrafttype import AircraftType
from ..dcs.groundunittype import GroundUnitType
from ..radio.ICLSContainer import ICLSContainer
from ..radio.Link4Container import Link4Container
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
from ..radio.TacanContainer import TacanContainer
from ..utils import nautical_miles
from ..weather import Conditions
@@ -305,7 +310,7 @@ class ControlPointStatus(IntEnum):
Destroyed = auto()
StartingPosition = ShipGroup | StaticGroup | Airport | Point
StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
class ControlPoint(MissionTarget, SidcDescribable, ABC):
@@ -1161,7 +1166,9 @@ class Airfield(ControlPoint):
return ControlPointStatus.Functional
class NavalControlPoint(ControlPoint, ABC):
class NavalControlPoint(
ControlPoint, ABC, Link4Container, TacanContainer, ICLSContainer
):
@property
def is_fleet(self) -> bool:
return True
@@ -1392,7 +1399,7 @@ class OffMapSpawn(ControlPoint):
return ControlPointStatus.Functional
class Fob(ControlPoint):
class Fob(ControlPoint, RadioFrequencyContainer):
def __init__(
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
):