mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Setup default radio channels for player flights.
This commit is contained in:
parent
010d505f04
commit
a9e65cc83d
@ -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
|
||||||
|
|||||||
182
gen/aircraft.py
182
gen/aircraft.py
@ -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:
|
||||||
|
|||||||
@ -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) \
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user