Merge remote-tracking branch 'khopa/develop' into helipads

# Conflicts:
#	changelog.md
This commit is contained in:
Khopa
2021-09-08 21:56:45 +02:00
132 changed files with 2856 additions and 648 deletions

View File

@@ -57,6 +57,7 @@ from dcs.task import (
Transport,
WeaponType,
TargetType,
Nothing,
)
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import Event, TriggerOnce, TriggerRule
@@ -92,7 +93,7 @@ from gen.flights.flight import (
from gen.lasercoderegistry import LaserCodeRegistry
from gen.radios import RadioFrequency, RadioRegistry
from gen.runways import RunwayData
from gen.tacan import TacanBand, TacanRegistry
from gen.tacan import TacanBand, TacanRegistry, TacanUsage
from .airsupport import AirSupport, AwacsInfo, TankerInfo
from .callsigns import callsign_for_support_unit
from .flights.flightplan import (
@@ -437,7 +438,7 @@ class AircraftConflictGenerator:
if isinstance(flight.flight_plan, RefuelingFlightPlan):
callsign = callsign_for_support_unit(group)
tacan = self.tacan_registy.alloc_for_band(TacanBand.Y)
tacan = self.tacan_registy.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
self.air_support.tankers.append(
TankerInfo(
group_name=str(group.name),
@@ -656,41 +657,36 @@ class AircraftConflictGenerator:
for squadron in control_point.squadrons:
try:
self._spawn_unused_at(control_point, country, faction, squadron)
self._spawn_unused_for(squadron, country, faction)
except NoParkingSlotError:
# If we run out of parking, stop spawning aircraft.
return
def _spawn_unused_at(
self,
control_point: Airfield,
country: Country,
faction: Faction,
squadron: Squadron,
def _spawn_unused_for(
self, squadron: Squadron, country: Country, faction: Faction
) -> None:
assert isinstance(squadron.location, Airfield)
for _ in range(squadron.untasked_aircraft):
# Creating a flight even those this isn't a fragged mission lets us
# reuse the existing debriefing code.
# TODO: Special flight type?
flight = Flight(
Package(control_point),
Package(squadron.location),
faction.country,
squadron,
1,
FlightType.BARCAP,
"Cold",
departure=control_point,
arrival=control_point,
divert=None,
)
group = self._generate_at_airport(
name=namegen.next_aircraft_name(country, control_point.id, flight),
name=namegen.next_aircraft_name(country, flight.departure.id, flight),
side=country,
unit_type=squadron.aircraft.dcs_unit_type,
count=1,
start_type="Cold",
airport=control_point.airport,
airport=squadron.location.airport,
)
self._setup_livery(flight, group)
@@ -1153,6 +1149,23 @@ class AircraftConflictGenerator:
restrict_jettison=True,
)
def configure_ferry(
self,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
group.task = Nothing.name
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior(
flight,
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True,
)
def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None:
logging.error(f"Unhandled flight type: {flight.flight_type}")
self.configure_behavior(flight, group)
@@ -1197,6 +1210,8 @@ class AircraftConflictGenerator:
self.configure_oca_strike(group, package, flight, dynamic_runways)
elif flight_type == FlightType.TRANSPORT:
self.configure_transport(group, package, flight, dynamic_runways)
elif flight_type == FlightType.FERRY:
self.configure_ferry(group, package, flight, dynamic_runways)
else:
self.configure_unknown_task(group, flight)
@@ -1783,6 +1798,8 @@ class LandingPointBuilder(PydcsWaypointBuilder):
waypoint = super().build()
waypoint.type = "Land"
waypoint.action = PointAction.Landing
if (control_point := self.waypoint.control_point) is not None:
waypoint.airdrome_id = control_point.airdrome_id_for_landing
return waypoint
@@ -1792,6 +1809,8 @@ class CargoStopBuilder(PydcsWaypointBuilder):
waypoint.type = "LandingReFuAr"
waypoint.action = PointAction.LandingReFuAr
waypoint.landing_refuel_rearm_time = 2 # Minutes.
if (control_point := self.waypoint.control_point) is not None:
waypoint.airdrome_id = control_point.airdrome_id_for_landing
return waypoint

View File

@@ -22,7 +22,7 @@ from .conflictgen import Conflict
from .flights.ai_flight_planner_db import AEWC_CAPABLE
from .naming import namegen
from .radios import RadioRegistry
from .tacan import TacanBand, TacanRegistry
from .tacan import TacanBand, TacanRegistry, TacanUsage
if TYPE_CHECKING:
from game import Game
@@ -89,7 +89,9 @@ class AirSupportConflictGenerator:
# TODO: Make loiter altitude a property of the unit type.
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tacan = self.tacan_registry.alloc_for_band(
TacanBand.Y, TacanUsage.AirToAir
)
tanker_heading = Heading.from_degrees(
self.conflict.red_cp.position.heading_between_point(
self.conflict.blue_cp.position

View File

@@ -143,9 +143,17 @@ class GroundConflictGenerator:
# Add JTAC
if self.game.blue.faction.has_jtac:
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
code: int = self.laser_code_registry.get_next_laser_code()
code: int
freq = self.radio_registry.alloc_uhf()
# If the option fc3LaserCode is enabled, force all JTAC
# laser codes to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs.
# Otherwise use 1688 for the first JTAC, 1687 for the second etc.
if self.game.settings.plugins["plugins.jtacautolase.fc3LaserCode"]:
code = 1113
else:
code = self.laser_code_registry.get_next_laser_code()
utype = self.game.blue.faction.jtac_unit
if utype is None:
utype = AircraftType.named("MQ-9 Reaper")

View File

@@ -183,6 +183,7 @@ class Package:
FlightType.TARCAP,
FlightType.BARCAP,
FlightType.AEWC,
FlightType.FERRY,
FlightType.REFUELING,
FlightType.SWEEP,
FlightType.ESCORT,

View File

@@ -70,6 +70,7 @@ class FlightType(Enum):
TRANSPORT = "Transport"
SEAD_ESCORT = "SEAD Escort"
REFUELING = "Refueling"
FERRY = "Ferry"
def __str__(self) -> str:
return self.value
@@ -159,6 +160,7 @@ class FlightWaypoint:
x: float,
y: float,
alt: Distance = meters(0),
control_point: Optional[ControlPoint] = None,
) -> None:
"""Creates a flight waypoint.
@@ -168,11 +170,14 @@ class FlightWaypoint:
y: Y coordinate of the waypoint.
alt: Altitude of the waypoint. By default this is MSL, but it can be
changed to AGL by setting alt_type to "RADIO"
control_point: The control point to associate with this waypoint. Needed for
landing points.
"""
self.waypoint_type = waypoint_type
self.x = x
self.y = y
self.alt = alt
self.control_point = control_point
self.alt_type = "BARO"
self.name = ""
# TODO: Merge with pretty_name.
@@ -282,8 +287,6 @@ class Flight:
count: int,
flight_type: FlightType,
start_type: str,
departure: ControlPoint,
arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None,
cargo: Optional[TransferOrder] = None,
@@ -297,8 +300,8 @@ class Flight:
self.roster = FlightRoster(self.squadron, initial_size=count)
else:
self.roster = roster
self.departure = departure
self.arrival = arrival
self.departure = self.squadron.location
self.arrival = self.squadron.arrival
self.divert = divert
self.flight_type = flight_type
# TODO: Replace with FlightPlan.

View File

@@ -37,10 +37,8 @@ from game.theater.theatergroundobject import (
NavalGroundObject,
BuildingGroundObject,
)
from game.threatzones import ThreatZones
from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime
@@ -836,6 +834,39 @@ class AirliftFlightPlan(FlightPlan):
return self.package.time_over_target
@dataclass(frozen=True)
class FerryFlightPlan(FlightPlan):
takeoff: FlightWaypoint
nav_to_destination: list[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to_destination
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.land
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
# TOT planning isn't really useful for ferries. They're behind the front
# lines so no need to wait for escorts or for other missions to complete.
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
return None
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
@dataclass(frozen=True)
class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint]
@@ -958,6 +989,8 @@ class FlightPlanBuilder:
return self.generate_transport(flight)
elif task == FlightType.REFUELING:
return self.generate_refueling_racetrack(flight)
elif task == FlightType.FERRY:
return self.generate_ferry(flight)
raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None:
@@ -1244,6 +1277,42 @@ class FlightPlanBuilder:
bullseye=builder.bullseye(),
)
def generate_ferry(self, flight: Flight) -> FerryFlightPlan:
"""Generate a ferry flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
if flight.departure == flight.arrival:
raise PlanningError(
f"Cannot plan ferry flight: departure and arrival are both "
f"{flight.departure}"
)
altitude_is_agl = flight.unit_type.dcs_unit_type.helicopter
altitude = (
feet(1500)
if altitude_is_agl
else flight.unit_type.preferred_patrol_altitude
)
builder = WaypointBuilder(flight, self.coalition)
return FerryFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to_destination=builder.nav_path(
flight.departure.position,
flight.arrival.position,
altitude,
altitude_is_agl,
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
def cap_racetrack_for_objective(
self, location: MissionTarget, barcap: bool
) -> Tuple[Point, Point]:

View File

@@ -110,7 +110,11 @@ class WaypointBuilder:
waypoint.pretty_name = "Exit theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT, position.x, position.y, meters(0)
FlightWaypointType.LANDING_POINT,
position.x,
position.y,
meters(0),
control_point=arrival,
)
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
@@ -139,7 +143,11 @@ class WaypointBuilder:
altitude_type = "RADIO"
waypoint = FlightWaypoint(
FlightWaypointType.DIVERT, position.x, position.y, altitude
FlightWaypointType.DIVERT,
position.x,
position.y,
altitude,
control_point=divert,
)
waypoint.alt_type = altitude_type
waypoint.name = "DIVERT"
@@ -488,6 +496,7 @@ class WaypointBuilder:
control_point.position.x,
control_point.position.y,
meters(0),
control_point=control_point,
)
waypoint.alt_type = "RADIO"
waypoint.name = "DROP OFF"

View File

@@ -59,7 +59,7 @@ from game.unitmap import UnitMap
from game.utils import Heading, feet, knots, mps
from .radios import RadioFrequency, RadioRegistry
from .runways import RunwayData
from .tacan import TacanBand, TacanChannel, TacanRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
if TYPE_CHECKING:
from game import Game
@@ -378,7 +378,9 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
for unit in group.units[1:]:
ship_group.add_unit(self.create_ship(unit, atc))
tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
tacan = self.tacan_registry.alloc_for_band(
TacanBand.X, TacanUsage.TransmitReceive
)
tacan_callsign = self.tacan_callsign()
icls = next(self.icls_alloc)

View File

@@ -43,209 +43,406 @@ ALPHA_MILITARY = [
]
ANIMALS: tuple[str, ...] = (
"SHARK",
"TORTOISE",
"BAT",
"PANGOLIN",
"AARDVARK",
"AARDWOLF",
"MONKEY",
"BUFFALO",
"DOG",
"BOBCAT",
"LYNX",
"PANTHER",
"TIGER",
"LION",
"OWL",
"BUTTERFLY",
"BISON",
"DUCK",
"COBRA",
"MAMBA",
"DOLPHIN",
"PHEASANT",
"ADDER",
"ALBACORE",
"ALBATROSS",
"ALLIGATOR",
"ALPACA",
"ANACONDA",
"ANOLE",
"ANTEATER",
"ANTELOPE",
"ANTLION",
"ARAPAIMA",
"ARCHERFISH",
"ARGALI",
"ARMADILLO",
"RACOON",
"ZEBRA",
"ASP",
"AUROCHS",
"AXOLOTL",
"BABIRUSA",
"BABOON",
"BADGER",
"BANDICOOT",
"BARRACUDA",
"BARRAMUNDI",
"BASILISK",
"BASS",
"BAT",
"BEAR",
"BEAVER",
"BEETLE",
"BELUGA",
"BETTONG",
"BINTURONG",
"BISON",
"BLOODHOUND",
"BOA",
"BOBCAT",
"BONGO",
"BONITO",
"BUFFALO",
"BULLDOG",
"BULLFROG",
"BULLSHARK",
"BUMBLEBEE",
"BUNNY",
"BUTTERFLY",
"CAIMAN",
"CAMEL",
"CANARY",
"CAPYBARA",
"CARACAL",
"CARP",
"CASTOR",
"CAT",
"CATERPILLAR",
"CATFISH",
"CENTIPEDE",
"CHAMELEON",
"CHEETAH",
"CHICKEN",
"CHIMAERA",
"CICADA",
"CICHLID",
"CIVET",
"COBIA",
"COBRA",
"COCKATOO",
"COD",
"COELACANTH",
"COLT",
"CONDOR",
"COPPERHEAD",
"CORAL",
"CORGI",
"COTTONMOUTH",
"COUGAR",
"COW",
"COYOTE",
"FOX",
"LIGHTFOOT",
"COTTONMOUTH",
"TAURUS",
"VIPER",
"CASTOR",
"GIRAFFE",
"SNAKE",
"MONSTER",
"ALBATROSS",
"HAWK",
"DOVE",
"MOCKINGBIRD",
"GECKO",
"ORYX",
"GORILLA",
"HARAMBE",
"GOOSE",
"MAVERICK",
"HARE",
"JACKAL",
"LEOPARD",
"CAT",
"MUSK",
"ORCA",
"OCELOT",
"BEAR",
"PANDA",
"GULL",
"PENGUIN",
"PYTHON",
"RAVEN",
"DEER",
"MOOSE",
"REINDEER",
"SHEEP",
"GAZELLE",
"INSECT",
"VULTURE",
"WALLABY",
"KANGAROO",
"KOALA",
"KIWI",
"WHALE",
"FISH",
"RHINO",
"HIPPO",
"RAT",
"WOODPECKER",
"WORM",
"BABOON",
"YAK",
"SCORPIO",
"HORSE",
"POODLE",
"CENTIPEDE",
"CHICKEN",
"CHEETAH",
"CHAMELEON",
"CATFISH",
"CATERPILLAR",
"CARACAL",
"CAMEL",
"CAIMAN",
"BARRACUDA",
"BANDICOOT",
"ALLIGATOR",
"BONGO",
"CORAL",
"ELEPHANT",
"ANTELOPE",
"CRAB",
"CRANE",
"CRICKET",
"CROCODILE",
"CROW",
"CUTTLEFISH",
"DACHSHUND",
"DEER",
"DINGO",
"DIREWOLF",
"DODO",
"FLAMINGO",
"FERRET",
"FALCON",
"BULLDOG",
"DOG",
"DOLPHIN",
"DONKEY",
"IGUANA",
"TAMARIN",
"HARRIER",
"GRIZZLY",
"GREYHOUND",
"GRASSHOPPER",
"JAGUAR",
"LADYBUG",
"KOMODO",
"DOVE",
"DRACO",
"DRAGON",
"DRAGONFLY",
"DUCK",
"DUGONG",
"EAGLE",
"EARWIG",
"ECHIDNA",
"EEL",
"ELEPHANT",
"ELK",
"EMU",
"ERMINE",
"FALCON",
"FANGTOOTH",
"FAWN",
"FENNEC",
"FERRET",
"FINCH",
"FIREFLY",
"FISH",
"FLAMINGO",
"FLEA",
"FLOUNDER",
"FORGMOUTH",
"FOX",
"FRINGEHEAD",
"FROG",
"GAR",
"GAZELLE",
"GECKO",
"GENET",
"GERBIL",
"GHARIAL",
"GIBBON",
"GIRAFFE",
"GOOSE",
"GOPHER",
"GORILLA",
"GOSHAWK",
"GRASSHOPPER",
"GREYHOUND",
"GRIZZLY",
"GROUPER",
"GROUSE",
"GRYPHON",
"GUANACO",
"GULL",
"GUPPY",
"HADDOCK",
"HAGFISH",
"HALIBUT",
"HAMSTER",
"HARAMBE",
"HARE",
"HARRIER",
"HAWK",
"HEDGEHOG",
"HERMITCRAB",
"HERON",
"HERRING",
"HIPPO",
"HORNBILL",
"HORNET",
"HORSE",
"HUNTSMAN",
"HUSKY",
"HYENA",
"IBEX",
"IBIS",
"IGUANA",
"IMPALA",
"INSECT",
"IRUKANDJI",
"ISOPOD",
"JACKAL",
"JAGUAR",
"JELLYFISH",
"JERBOA",
"KAKAPO",
"KANGAROO",
"KATYDID",
"KEA",
"KINGFISHER",
"KITTEN",
"KIWI",
"KOALA",
"KOMODO",
"KRAIT",
"LADYBUG",
"LAMPREY",
"LEMUR",
"LEOPARD",
"LIGHTFOOT",
"LION",
"LIONFISH",
"LIZARD",
"LLAMA",
"LOACH",
"LOBSTER",
"OCTOPUS",
"MANATEE",
"MAGPIE",
"MACAW",
"OSTRICH",
"OYSTER",
"MOLE",
"MULE",
"MOTH",
"MONGOOSE",
"MOLLY",
"MEERKAT",
"MOUSE",
"PEACOCK",
"PIKE",
"ROBIN",
"RAGDOLL",
"PLATYPUS",
"PELICAN",
"PARROT",
"PORCUPINE",
"PIRANHA",
"PUMA",
"PUG",
"TAPIR",
"TERMITE",
"URCHIN",
"SHRIMP",
"TURKEY",
"TOUCAN",
"TETRA",
"HUSKY",
"STARFISH",
"SWAN",
"FROG",
"SQUIRREL",
"WALRUS",
"WARTHOG",
"CORGI",
"WEASEL",
"WOMBAT",
"WOLVERINE",
"MAMMOTH",
"TOAD",
"WOLF",
"ZEBU",
"SEAL",
"SKATE",
"JELLYFISH",
"MOSQUITO",
"LOCUST",
"LORIKEET",
"LUNGFISH",
"LYNX",
"MACAW",
"MAGPIE",
"MALLARD",
"MAMBA",
"MAMMOTH",
"MANATEE",
"MANDRILL",
"MANTA",
"MANTIS",
"MARE",
"MARLIN",
"MARMOT",
"MARTEN",
"MASTIFF",
"MASTODON",
"MAVERICK",
"MAYFLY",
"MEERKAT",
"MILLIPEDE",
"MINK",
"MOA",
"MOCKINGBIRD",
"MOLE",
"MOLERAT",
"MOLLY",
"MONGOOSE",
"MONKEY",
"MONKFISH",
"MONSTER",
"MOOSE",
"MORAY",
"MOSQUITO",
"MOTH",
"MOUSE",
"MUDSKIPPER",
"MULE",
"MUSK",
"MYNA",
"NARWHAL",
"NAUTILUS",
"NEWT",
"NIGHTINGALE",
"NUMBAT",
"OCELOT",
"OCTOPUS",
"OKAPI",
"OLM",
"OPAH",
"OPOSSUM",
"ORCA",
"ORYX",
"OSPREY",
"OSTRICH",
"OTTER",
"OWL",
"OX",
"OYSTER",
"PADDLEFISH",
"PADEMELON",
"PANDA",
"PANGOLIN",
"PANTHER",
"PARAKEET",
"PARROT",
"PEACOCK",
"PELICAN",
"PENGUIN",
"PERCH",
"PEREGRINE",
"PETRAL",
"PHEASANT",
"PIG",
"PIGEON",
"PIGLET",
"PIKE",
"PIRANHA",
"PLATYPUS",
"POODLE",
"PORCUPINE",
"PORPOISE",
"POSSUM",
"POTOROO",
"PRONGHORN",
"PUFFERFISH",
"PUFFIN",
"PUG",
"PUMA",
"PYTHON",
"QUAGGA",
"QUAIL",
"QUOKKA",
"QUOLL",
"RABBIT",
"RACOON",
"RAGDOLL",
"RAT",
"RATTLESNAKE",
"RAVEN",
"REINDEER",
"RHINO",
"ROACH",
"ROBIN",
"SABERTOOTH",
"SAILFISH",
"SALAMANDER",
"SALMON",
"SANDFLY",
"SARDINE",
"SAWFISH",
"SCARAB",
"SCORPION",
"SEAHORSE",
"SEAL",
"SEALION",
"SERVAL",
"SHARK",
"SHEEP",
"SHOEBILL",
"SHRIKE",
"SHRIMP",
"SIDEWINDER",
"SILKWORM",
"SKATE",
"SKINK",
"SKUNK",
"SLOTH",
"SLUG",
"SNAIL",
"HEDGEHOG",
"PIGLET",
"FENNEC",
"BADGER",
"ALPACA",
"DINGO",
"COLT",
"SKUNK",
"BUNNY",
"IMPALA",
"GUANACO",
"CAPYBARA",
"ELK",
"MINK",
"PRONGHORN",
"CROW",
"BUMBLEBEE",
"FAWN",
"OTTER",
"SNAKE",
"SNAPPER",
"SNOOK",
"SPARROW",
"SPIDER",
"SPRINGBOK",
"SQUID",
"SQUIRREL",
"STAGHORN",
"STARFISH",
"STINGRAY",
"STINKBUG",
"STOUT",
"STURGEON",
"SUGARGLIDER",
"SUNBEAR",
"SWALLOW",
"SWAN",
"SWIFT",
"SWORDFISH",
"TAIPAN",
"TAKAHE",
"TAMARIN",
"TANG",
"TAPIR",
"TARANTULA",
"TARPON",
"TARSIER",
"TAURUS",
"TERMITE",
"TERRIER",
"TETRA",
"THRUSH",
"THYLACINE",
"TIGER",
"TOAD",
"TORTOISE",
"TOUCAN",
"TREADFIN",
"TREVALLY",
"TRIGGERFISH",
"TROUT",
"TUATARA",
"TUNA",
"TURKEY",
"TURTLE",
"URCHIN",
"VIPER",
"VULTURE",
"WALLABY",
"WALLAROO",
"WALLEYE",
"WALRUS",
"WARTHOG",
"WASP",
"WATERBUCK",
"JERBOA",
"KITTEN",
"ARGALI",
"OX",
"MARE",
"FINCH",
"BASILISK",
"GOPHER",
"HAMSTER",
"CANARY",
"WEASEL",
"WEEVIL",
"WEKA",
"WHALE",
"WILDCAT",
"WILDEBEEST",
"WOLF",
"WOLFHOUND",
"WOLVERINE",
"WOMBAT",
"WOODCHUCK",
"ANACONDA",
"WOODPECKER",
"WORM",
"WRASSE",
"WYVERN",
"YAK",
"ZEBRA",
"ZEBU",
)

View File

@@ -4,13 +4,37 @@ from enum import Enum
from typing import Dict, Iterator, Set
class TacanUsage(Enum):
TransmitReceive = "transmit receive"
AirToAir = "air to air"
class TacanBand(Enum):
X = "X"
Y = "Y"
def range(self) -> Iterator["TacanChannel"]:
"""Returns an iterator over the channels in this band."""
return (TacanChannel(x, self) for x in range(1, 100))
return (TacanChannel(x, self) for x in range(1, 126 + 1))
def valid_channels(self, usage: TacanUsage) -> Iterator["TacanChannel"]:
for x in self.range():
if x.number not in UNAVAILABLE[usage][self]:
yield x
# Avoid certain TACAN channels for various reasons
# https://forums.eagle.ru/topic/276390-datalink-issue/
UNAVAILABLE = {
TacanUsage.TransmitReceive: {
TacanBand.X: set(range(2, 30 + 1)) | set(range(47, 63 + 1)),
TacanBand.Y: set(range(2, 30 + 1)) | set(range(64, 92 + 1)),
},
TacanUsage.AirToAir: {
TacanBand.X: set(range(1, 36 + 1)) | set(range(64, 99 + 1)),
TacanBand.Y: set(range(1, 36 + 1)) | set(range(64, 99 + 1)),
},
}
@dataclass(frozen=True)
@@ -41,25 +65,30 @@ class TacanRegistry:
def __init__(self) -> None:
self.allocated_channels: Set[TacanChannel] = set()
self.band_allocators: Dict[TacanBand, Iterator[TacanChannel]] = {}
self.allocators: Dict[TacanBand, Dict[TacanUsage, Iterator[TacanChannel]]] = {}
for band in TacanBand:
self.band_allocators[band] = band.range()
self.allocators[band] = {}
for usage in TacanUsage:
self.allocators[band][usage] = band.valid_channels(usage)
def alloc_for_band(self, band: TacanBand) -> TacanChannel:
def alloc_for_band(
self, band: TacanBand, intended_usage: TacanUsage
) -> TacanChannel:
"""Allocates a TACAN channel in the given band.
Args:
band: The TACAN band to allocate a channel for.
intended_usage: What the caller intends to use the tacan channel for.
Returns:
A TACAN channel in the given band.
Raises:
OutOfChannelsError: All channels compatible with the given radio are
OutOfTacanChannelsError: All channels compatible with the given radio are
already allocated.
"""
allocator = self.band_allocators[band]
allocator = self.allocators[band][intended_usage]
try:
while (channel := next(allocator)) in self.allocated_channels:
pass
@@ -67,7 +96,7 @@ class TacanRegistry:
except StopIteration:
raise OutOfTacanChannelsError(band)
def reserve(self, channel: TacanChannel) -> None:
def mark_unavailable(self, channel: TacanChannel) -> None:
"""Reserves the given channel.
Reserving a channel ensures that it will not be allocated in the future.
@@ -76,7 +105,7 @@ class TacanRegistry:
channel: The channel to reserve.
Raises:
ChannelInUseError: The given frequency is already in use.
TacanChannelInUseError: The given channel is already in use.
"""
if channel in self.allocated_channels:
raise TacanChannelInUseError(channel)