Setup default radio channels for player flights.

This commit is contained in:
Dan Albert 2020-09-01 00:03:37 -07:00
parent 010d505f04
commit a9e65cc83d
4 changed files with 289 additions and 82 deletions

View File

@ -255,7 +255,7 @@ class Operation:
load_dcs_libe.add_action(DoScript(String(script))) load_dcs_libe.add_action(DoScript(String(script)))
self.current_mission.triggerrules.triggers.append(load_dcs_libe) self.current_mission.triggerrules.triggers.append(load_dcs_libe)
kneeboard_generator = KneeboardGenerator(self.current_mission, self.game) kneeboard_generator = KneeboardGenerator(self.current_mission)
# Briefing Generation # Briefing Generation
for tanker in self.airsupportgen.air_support.tankers: for tanker in self.airsupportgen.air_support.tankers:
@ -269,9 +269,82 @@ class Operation:
self.briefinggen.append_frequency(awacs.callsign, awacs.freq) self.briefinggen.append_frequency(awacs.callsign, awacs.freq)
kneeboard_generator.add_awacs(awacs) kneeboard_generator.add_awacs(awacs)
self.assign_channels_to_flights()
# Generate the briefing # Generate the briefing
self.briefinggen.generate() self.briefinggen.generate()
for region, code, name in self.game.jtacs: for region, code, name in self.game.jtacs:
kneeboard_generator.add_jtac(name, region, code) kneeboard_generator.add_jtac(name, region, code)
kneeboard_generator.generate() kneeboard_generator.generate(self.airgen.flights)
def assign_channels_to_flights(self) -> None:
"""Assigns preset radio channels for client flights."""
for flight in self.airgen.flights:
if not flight.client_units:
continue
self.assign_channels_to_flight(flight)
def assign_channels_to_flight(self, flight: FlightData) -> 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
# Intra-flight channel is set up when the flight is created, however we
# do need to make sure we don't overwrite it. For cases where the
# inter-flight and intra-flight radios share presets (the AV-8B only has
# one set of channels, even though it can use two channels
# simultaneously), start assigning channels at 2.
radio_id = aircraft_data.inter_flight_radio_index
if aircraft_data.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))
# TODO: Fix departure/arrival to support carriers.
if flight.departure is not None:
try:
departure = AIRFIELD_DATA[flight.departure.name]
flight.assign_channel(
radio_id, next(channel_alloc), departure.atc.uhf)
except KeyError:
pass
# TODO: If there ever are multiple AWACS, limit to mission relevant.
for awacs in self.airsupportgen.air_support.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
# TODO: Fix departure/arrival to support carriers.
if flight.arrival is not None and flight.arrival != flight.departure:
try:
arrival = AIRFIELD_DATA[flight.arrival.name]
flight.assign_channel(
radio_id, next(channel_alloc), arrival.atc.uhf)
except KeyError:
pass
try:
# TODO: Skip incompatible tankers.
for tanker in self.airsupportgen.air_support.tankers:
flight.assign_channel(
radio_id, next(channel_alloc), tanker.freq)
if flight.divert is not None:
try:
divert = AIRFIELD_DATA[flight.divert.name]
flight.assign_channel(
radio_id, next(channel_alloc), divert.atc.uhf)
except KeyError:
pass
except StopIteration:
# Any remaining channels are nice-to-haves, but not necessary for
# the few aircraft with a small number of channels available.
pass

View File

@ -1,15 +1,21 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict from typing import Dict, List, Optional, Tuple
from game.data.cap_capabilities_db import GUNFIGHTERS from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings from game.settings import Settings
from game.utils import nm_to_meter from game.utils import nm_to_meter
from gen.flights.ai_flight_planner import FlightPlanner from gen.flights.ai_flight_planner import FlightPlanner
from gen.flights.flight import Flight, FlightType, FlightWaypointType from gen.flights.flight import (
Flight,
FlightType,
FlightWaypoint,
FlightWaypointType,
)
from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry
from pydcs.dcs import helicopters from pydcs.dcs import helicopters
from pydcs.dcs.action import ActivateGroup, AITaskPush, MessageToAll from pydcs.dcs.action import ActivateGroup, AITaskPush, MessageToAll
from pydcs.dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone from pydcs.dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone
from pydcs.dcs.flyingunit import FlyingUnit
from pydcs.dcs.helicopters import helicopter_map, UH_1H from pydcs.dcs.helicopters import helicopter_map, UH_1H
from pydcs.dcs.mission import Mission, StartType from pydcs.dcs.mission import Mission, StartType
from pydcs.dcs.planes import ( from pydcs.dcs.planes import (
@ -24,9 +30,9 @@ from pydcs.dcs.planes import (
SpitfireLFMkIX, SpitfireLFMkIX,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
) )
from pydcs.dcs.terrain.terrain import NoParkingSlotError from pydcs.dcs.terrain.terrain import Airport, NoParkingSlotError
from pydcs.dcs.triggers import TriggerOnce, Event from pydcs.dcs.triggers import TriggerOnce, Event
from pydcs.dcs.unittype import UnitType from pydcs.dcs.unittype import FlyingType, UnitType
from .conflictgen import * from .conflictgen import *
from .naming import * from .naming import *
@ -59,12 +65,40 @@ class AircraftData:
#: The type of radio used for intra-flight communications. #: The type of radio used for intra-flight communications.
intra_flight_radio: Radio intra_flight_radio: Radio
#: 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]
# Indexed by the id field of the pydcs PlaneType. # Indexed by the id field of the pydcs PlaneType.
AIRCRAFT_DATA: Dict[str, AircraftData] = { AIRCRAFT_DATA: Dict[str, AircraftData] = {
"A-10C": AircraftData(get_radio("AN/ARC-186(V) AM")), "A-10C": AircraftData(
"F-16C_50": AircraftData(get_radio("AN/ARC-222")), get_radio("AN/ARC-186(V) AM"),
"F/A-18C": AircraftData(get_radio("AN/ARC-210")), # The A-10's radio works differently than most aircraft. Doesn't seem to
# be a way to set these from the mission editor, let alone pydcs.
inter_flight_radio_index=None,
intra_flight_radio_index=None
),
"F-16C_50": AircraftData(
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.
inter_flight_radio_index=1,
intra_flight_radio_index=2
),
"FA-18C_hornet": AircraftData(
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.
inter_flight_radio_index=2,
intra_flight_radio_index=1
),
} }
@ -98,6 +132,100 @@ def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
return UHF_FALLBACK_CHANNEL return UHF_FALLBACK_CHANNEL
@dataclass(frozen=True)
class ChannelAssignment:
radio_id: int
channel: int
@property
def radio_name(self) -> str:
"""Returns the name of the radio, i.e. COM1."""
return f"COM{self.radio_id}"
@dataclass
class FlightData:
"""Details of a planned flight."""
#: List of playable units in the flight.
client_units: List[FlyingUnit]
# TODO: Arrival and departure should not be optional, but carriers don't count.
#: Arrival airport.
arrival: Optional[Airport]
#: Departure airport.
departure: Optional[Airport]
#: Diver airport.
divert: Optional[Airport]
#: Waypoints of the flight plan.
waypoints: List[FlightWaypoint]
#: Radio frequency for intra-flight communications.
intra_flight_channel: RadioFrequency
#: Map of radio frequencies to their assigned radio and channel, if any.
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment]
def __init__(self, client_units: List[FlyingUnit], arrival: Airport,
departure: Airport, divert: Optional[Airport],
waypoints: List[FlightWaypoint],
intra_flight_channel: RadioFrequency) -> None:
self.client_units = client_units
self.arrival = arrival
self.departure = departure
self.divert = divert
self.waypoints = waypoints
self.intra_flight_channel = intra_flight_channel
self.frequency_to_channel_map = {}
self.assign_intra_flight_channel()
def assign_intra_flight_channel(self) -> None:
"""Assigns a channel to the intra-flight frequency."""
if not self.client_units:
return
# pydcs will actually set up the channel for us, but we want to make
# sure that it ends up in frequency_to_channel_map.
try:
data = AIRCRAFT_DATA[self.aircraft_type.id]
self.assign_channel(
data.intra_flight_radio_index, 1, self.intra_flight_channel)
except KeyError:
logging.warning(f"No aircraft data for {self.aircraft_type.id}")
@property
def aircraft_type(self) -> FlyingType:
"""Returns the type of aircraft in this flight."""
return self.client_units[0].unit_type
def num_radio_channels(self, radio_id: int) -> int:
"""Returns the number of preset channels for the given radio."""
return self.client_units[0].num_radio_channels(radio_id)
def channel_for(
self, frequency: RadioFrequency) -> Optional[ChannelAssignment]:
"""Returns the radio and channel number for the given frequency."""
return self.frequency_to_channel_map.get(frequency, None)
def assign_channel(self, radio_id: int, channel_id: int,
frequency: RadioFrequency) -> None:
"""Assigns a preset radio channel to the given frequency."""
for unit in self.client_units:
unit.set_radio_channel_preset(radio_id, channel_id, frequency.mhz)
# One frequency could be bound to multiple channels. Prefer the first,
# since with the current implementation it will be the lowest numbered
# channel.
if frequency not in self.frequency_to_channel_map:
self.frequency_to_channel_map[frequency] = ChannelAssignment(
radio_id, channel_id
)
class AircraftConflictGenerator: class AircraftConflictGenerator:
escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]] escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]]
@ -109,14 +237,26 @@ class AircraftConflictGenerator:
self.conflict = conflict self.conflict = conflict
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.escort_targets = [] self.escort_targets = []
self.flights: List[FlightData] = []
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: def get_intra_flight_channel(
self, airframe: UnitType) -> Tuple[int, RadioFrequency]:
"""Allocates an intra-flight channel to a group.
Args:
airframe: The type of aircraft a channel should be allocated for.
Returns:
A tuple of the radio index (for aircraft with multiple radios) and
the frequency of the intra-flight channel.
"""
try: try:
aircraft_data = AIRCRAFT_DATA[airframe.id] aircraft_data = AIRCRAFT_DATA[airframe.id]
return self.radio_registry.alloc_for_radio( channel = self.radio_registry.alloc_for_radio(
aircraft_data.intra_flight_radio) aircraft_data.intra_flight_radio)
return aircraft_data.intra_flight_radio_index, channel
except KeyError: except KeyError:
return get_fallback_channel(airframe) return 1, get_fallback_channel(airframe)
def _start_type(self) -> StartType: def _start_type(self) -> StartType:
return self.settings.cold_start and StartType.Cold or StartType.Warm return self.settings.cold_start and StartType.Cold or StartType.Warm
@ -156,12 +296,15 @@ class AircraftConflictGenerator:
for unit_instance in group.units: for unit_instance in group.units:
unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type] unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type]
clients: List[FlyingUnit] = []
single_client = flight.client_count == 1 single_client = flight.client_count == 1
for idx in range(0, min(len(group.units), flight.client_count)): for idx in range(0, min(len(group.units), flight.client_count)):
unit = group.units[idx]
clients.append(unit)
if single_client: if single_client:
group.units[idx].set_player() unit.set_player()
else: else:
group.units[idx].set_client() unit.set_client()
# Do not generate player group with late activation. # Do not generate player group with late activation.
if group.late_activation: if group.late_activation:
@ -169,14 +312,21 @@ class AircraftConflictGenerator:
# Set up F-14 Client to have pre-stored alignement # Set up F-14 Client to have pre-stored alignement
if unit_type is F_14B: if unit_type is F_14B:
group.units[idx].set_property(F_14B.Properties.INSAlignmentStored.id, True) unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
channel = self.get_intra_flight_channel(unit_type) radio_id, channel = self.get_intra_flight_channel(unit_type)
group.set_frequency(channel.mhz) group.set_frequency(channel.mhz, radio_id)
flight.intra_flight_channel = channel self.flights.append(FlightData(
client_units=clients,
departure=flight.from_cp.airport,
arrival=flight.from_cp.airport,
divert=None,
waypoints=flight.points,
intra_flight_channel=channel
))
# Special case so Su 33 carrier take off # Special case so Su 33 carrier take off
if unit_type is Su_33: if unit_type is Su_33:

View File

@ -1,10 +1,8 @@
from enum import Enum from enum import Enum
from typing import List, Optional from typing import List
from pydcs.dcs.unittype import UnitType
from game import db from game import db
from gen.radios import RadioFrequency from pydcs.dcs.unittype import UnitType
class FlightType(Enum): class FlightType(Enum):
@ -96,13 +94,6 @@ class Flight:
# How long before this flight should take off # How long before this flight should take off
scheduled_in = 0 scheduled_in = 0
# Populated during mission generation time by AircraftConflictGenerator.
# TODO: Decouple radio planning from the Flight.
# Make AircraftConflictGenerator generate a FlightData object that is
# returned to the Operation rather than relying on the Flight object, which
# represents a game UI flight rather than a fully planned flight.
intra_flight_channel: Optional[RadioFrequency]
def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType): def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType):
self.unit_type = unit_type self.unit_type = unit_type
self.count = count self.count = count
@ -112,7 +103,6 @@ class Flight:
self.targets = [] self.targets = []
self.loadout = {} self.loadout = {}
self.start_type = "Runway" self.start_type = "Runway"
self.intra_flight_channel = None
def __repr__(self): def __repr__(self):
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \ return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \

View File

@ -34,9 +34,9 @@ from pydcs.dcs.mission import Mission
from pydcs.dcs.terrain.terrain import Airport from pydcs.dcs.terrain.terrain import Airport
from pydcs.dcs.unittype import FlyingType from pydcs.dcs.unittype import FlyingType
from . import units from . import units
from .aircraft import FlightData
from .airfields import AIRFIELD_DATA from .airfields import AIRFIELD_DATA
from .airsupportgen import AwacsInfo, TankerInfo from .airsupportgen import AwacsInfo, TankerInfo
from .flights.flight import Flight
from .radios import RadioFrequency from .radios import RadioFrequency
@ -96,24 +96,6 @@ class KneeboardPage:
raise NotImplementedError raise NotImplementedError
class AirfieldInfo:
def __init__(self, airfield: Airport) -> None:
self.airport = airfield
# TODO: Implement logic for picking preferred runway.
runway = airfield.runways[0]
runway_side = ["", "L", "R"][runway.leftright]
self.runway = f"{runway.heading}{runway_side}"
try:
extra_data = AIRFIELD_DATA[airfield.name]
self.atc = extra_data.atc.uhf or ""
self.tacan = extra_data.tacan or ""
self.ils = extra_data.ils_freq(self.runway) or ""
except KeyError:
self.atc = ""
self.ils = ""
self.tacan = ""
@dataclass @dataclass
class CommInfo: class CommInfo:
"""Communications information for the kneeboard.""" """Communications information for the kneeboard."""
@ -131,21 +113,15 @@ class JtacInfo:
class BriefingPage(KneeboardPage): class BriefingPage(KneeboardPage):
"""A kneeboard page containing briefing information.""" """A kneeboard page containing briefing information."""
def __init__(self, flight: Flight, comms: List[CommInfo], def __init__(self, flight: FlightData, comms: List[CommInfo],
awacs: List[AwacsInfo], tankers: List[TankerInfo], awacs: List[AwacsInfo], tankers: List[TankerInfo],
jtacs: List[JtacInfo]) -> None: jtacs: List[JtacInfo]) -> None:
self.flight = flight self.flight = flight
self.comms = comms self.comms = list(comms)
self.awacs = awacs self.awacs = awacs
self.tankers = tankers self.tankers = tankers
self.jtacs = jtacs self.jtacs = jtacs
if self.flight.intra_flight_channel is not None: self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
self.comms.append(
CommInfo("Flight", self.flight.intra_flight_channel)
)
self.departure = flight.from_cp.airport
self.arrival = flight.from_cp.airport
self.divert: Optional[Airport] = None
def write(self, path: Path) -> None: def write(self, path: Path) -> None:
writer = KneeboardPageWriter() writer = KneeboardPageWriter()
@ -156,14 +132,14 @@ class BriefingPage(KneeboardPage):
# TODO: Handle carriers. # TODO: Handle carriers.
writer.heading("Airfield Info") writer.heading("Airfield Info")
writer.table([ writer.table([
self.airfield_info_row("Departure", self.departure), self.airfield_info_row("Departure", self.flight.departure),
self.airfield_info_row("Arrival", self.arrival), self.airfield_info_row("Arrival", self.flight.arrival),
self.airfield_info_row("Divert", self.divert), self.airfield_info_row("Divert", self.flight.divert),
], headers=["", "Airbase", "ATC", "TCN", "ILS", "RWY"]) ], headers=["", "Airbase", "ATC", "TCN", "ILS", "RWY"])
writer.heading("Flight Plan") writer.heading("Flight Plan")
flight_plan = [] flight_plan = []
for num, waypoint in enumerate(self.flight.points): for num, waypoint in enumerate(self.flight.waypoints):
alt = int(units.meters_to_feet(waypoint.alt)) alt = int(units.meters_to_feet(waypoint.alt))
flight_plan.append([num, waypoint.pretty_name, str(alt)]) flight_plan.append([num, waypoint.pretty_name, str(alt)])
writer.table(flight_plan, headers=["STPT", "Action", "Alt"]) writer.table(flight_plan, headers=["STPT", "Action", "Alt"])
@ -171,13 +147,13 @@ class BriefingPage(KneeboardPage):
writer.heading("Comm Ladder") writer.heading("Comm Ladder")
comms = [] comms = []
for comm in self.comms: for comm in self.comms:
comms.append([comm.name, comm.freq]) comms.append([comm.name, self.format_frequency(comm.freq)])
writer.table(comms, headers=["Name", "UHF"]) writer.table(comms, headers=["Name", "UHF"])
writer.heading("AWACS") writer.heading("AWACS")
awacs = [] awacs = []
for a in self.awacs: for a in self.awacs:
awacs.append([a.callsign, a.freq]) awacs.append([a.callsign, self.format_frequency(a.freq)])
writer.table(awacs, headers=["Callsign", "UHF"]) writer.table(awacs, headers=["Callsign", "UHF"])
writer.heading("Tankers") writer.heading("Tankers")
@ -187,7 +163,7 @@ class BriefingPage(KneeboardPage):
tanker.callsign, tanker.callsign,
tanker.variant, tanker.variant,
tanker.tacan, tanker.tacan,
tanker.freq, self.format_frequency(tanker.freq),
]) ])
writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"]) writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"])
@ -213,23 +189,42 @@ class BriefingPage(KneeboardPage):
""" """
if airfield is None: if airfield is None:
return [row_title, "", "", "", "", ""] return [row_title, "", "", "", "", ""]
info = AirfieldInfo(airfield)
# TODO: Implement logic for picking preferred runway.
runway = airfield.runways[0]
runway_side = ["", "L", "R"][runway.leftright]
runway_text = f"{runway.heading}{runway_side}"
try:
extra_data = AIRFIELD_DATA[airfield.name]
atc = self.format_frequency(extra_data.atc.uhf)
tacan = extra_data.tacan or ""
ils = extra_data.ils_freq(runway) or ""
except KeyError:
atc = ""
ils = ""
tacan = ""
return [ return [
row_title, row_title,
airfield.name, airfield.name,
info.atc, atc,
info.tacan, tacan,
info.ils, ils,
info.runway, runway_text,
] ]
def format_frequency(self, frequency: RadioFrequency) -> str:
channel = self.flight.channel_for(frequency)
if channel is None:
return str(frequency)
return f"{channel.radio_name} Ch {channel.channel}"
class KneeboardGenerator: class KneeboardGenerator:
"""Creates kneeboard pages for each client flight in the mission.""" """Creates kneeboard pages for each client flight in the mission."""
def __init__(self, mission: Mission, game) -> None: def __init__(self, mission: Mission) -> None:
self.mission = mission self.mission = mission
self.game = game
self.comms: List[CommInfo] = [] self.comms: List[CommInfo] = []
self.awacs: List[AwacsInfo] = [] self.awacs: List[AwacsInfo] = []
self.tankers: List[TankerInfo] = [] self.tankers: List[TankerInfo] = []
@ -271,11 +266,11 @@ class KneeboardGenerator:
# TODO: Radio info? Type? # TODO: Radio info? Type?
self.jtacs.append(JtacInfo(callsign, region, code)) self.jtacs.append(JtacInfo(callsign, region, code))
def generate(self) -> None: def generate(self, flights: List[FlightData]) -> None:
"""Generates a kneeboard per client flight.""" """Generates a kneeboard per client flight."""
temp_dir = Path("kneeboards") temp_dir = Path("kneeboards")
temp_dir.mkdir(exist_ok=True) temp_dir.mkdir(exist_ok=True)
for aircraft, pages in self.pages_by_airframe().items(): for aircraft, pages in self.pages_by_airframe(flights).items():
aircraft_dir = temp_dir / aircraft.id aircraft_dir = temp_dir / aircraft.id
aircraft_dir.mkdir(exist_ok=True) aircraft_dir.mkdir(exist_ok=True)
for idx, page in enumerate(pages): for idx, page in enumerate(pages):
@ -283,7 +278,7 @@ class KneeboardGenerator:
page.write(page_path) page.write(page_path)
self.mission.add_aircraft_kneeboard(aircraft, page_path) self.mission.add_aircraft_kneeboard(aircraft, page_path)
def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]: def pages_by_airframe(self, flights: List[FlightData]) -> Dict[FlyingType, List[KneeboardPage]]:
"""Returns a list of kneeboard pages per airframe in the mission. """Returns a list of kneeboard pages per airframe in the mission.
Only client flights will be included, but because DCS does not support Only client flights will be included, but because DCS does not support
@ -295,15 +290,14 @@ class KneeboardGenerator:
that aircraft. that aircraft.
""" """
all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list) all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list)
for cp in self.game.theater.controlpoints: for flight in flights:
if cp.id in self.game.planners.keys(): if not flight.client_units:
for flight in self.game.planners[cp.id].flights: continue
if flight.client_count > 0: all_flights[flight.aircraft_type].extend(
all_flights[flight.unit_type].extend( self.generate_flight_kneeboard(flight))
self.generate_flight_kneeboard(flight))
return all_flights return all_flights
def generate_flight_kneeboard(self, flight: Flight) -> List[KneeboardPage]: def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
"""Returns a list of kneeboard pages for the given flight.""" """Returns a list of kneeboard pages for the given flight."""
return [ return [
BriefingPage( BriefingPage(