diff --git a/game/data/alic.py b/game/data/alic.py new file mode 100644 index 00000000..de99075c --- /dev/null +++ b/game/data/alic.py @@ -0,0 +1,40 @@ +from dcs.unit import Unit +from dcs.vehicles import AirDefence + + +class AlicCodes: + CODES = { + AirDefence.EWR_1L13.id: 101, + AirDefence.EWR_55G6.id: 102, + AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR.id: 103, + AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id: 104, + AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR.id: 107, + AirDefence.SAM_SA_6_Kub_Long_Track_STR.id: 108, + AirDefence.MCC_SR_Sborka_Dog_Ear_SR.id: 109, + AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR.id: 110, + AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL.id: 115, + AirDefence.SAM_SA_8_Osa_Gecko_TEL.id: 117, + AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL.id: 118, + AirDefence.SAM_SA_15_Tor_Gauntlet.id: 119, + AirDefence.SAM_SA_19_Tunguska_Grison.id: 120, + AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id: 121, + AirDefence.SAM_P19_Flat_Face_SR__SA_2_3.id: 122, + AirDefence.SAM_SA_3_S_125_Low_Blow_TR.id: 123, + AirDefence.SAM_Rapier_Blindfire_TR.id: 124, + AirDefence.SAM_Rapier_LN.id: 125, + AirDefence.SAM_SA_2_S_75_Fan_Song_TR.id: 126, + AirDefence.HQ_7_Self_Propelled_LN.id: 127, + AirDefence.HQ_7_Self_Propelled_STR.id: 128, + AirDefence.SAM_Roland_ADS.id: 201, + AirDefence.SAM_Patriot_STR.id: 202, + AirDefence.SAM_Hawk_SR__AN_MPQ_50.id: 203, + AirDefence.SAM_Hawk_TR__AN_MPQ_46.id: 204, + AirDefence.SAM_Roland_EWR.id: 205, + AirDefence.SAM_Hawk_CWAR_AN_MPQ_55.id: 206, + AirDefence.SPAAA_Gepard.id: 207, + AirDefence.SPAAA_Vulcan_M163.id: 208, + } + + @classmethod + def code_for(cls, unit: Unit) -> int: + return cls.CODES[unit.type] diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 231c00ca..f781776e 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -30,10 +30,13 @@ from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Iterator from PIL import Image, ImageDraw, ImageFont from dcs.mission import Mission +from dcs.unit import Unit from dcs.unittype import FlyingType from tabulate import tabulate -from game.theater import ConflictTheater +from game.data.alic import AlicCodes +from game.db import find_unittype, unit_type_from_name +from game.theater import ConflictTheater, TheaterGroundObject, LatLon from game.utils import meters from .aircraft import AIRCRAFT_DATA, FlightData from .airsupportgen import AwacsInfo, TankerInfo @@ -42,7 +45,6 @@ from .flights.flight import FlightWaypoint, FlightWaypointType, FlightType from .radios import RadioFrequency from .runways import RunwayData - if TYPE_CHECKING: from game import Game @@ -138,6 +140,11 @@ class KneeboardPage: """Writes the kneeboard page to the given path.""" raise NotImplementedError + def format_ll(self, ll: LatLon) -> str: + ns = "N" if ll.latitude >= 0 else "S" + ew = "E" if ll.longitude >= 0 else "W" + return f"{ll.latitude:.4}°{ns} {ll.longitude:.4}°{ew}" + @dataclass(frozen=True) class NumberedWaypoint: @@ -415,21 +422,70 @@ class BriefingPage(KneeboardPage): return local_time.strftime(f"%H:%M:%S") -class TargetInfoPage(KneeboardPage): - """A kneeboard page containing target information.""" +class SeadTaskPage(KneeboardPage): + """A kneeboard page containing SEAD/DEAD target information.""" + + def __init__( + self, flight: FlightData, dark_kneeboard: bool, theater: ConflictTheater + ) -> None: + self.flight = flight + self.dark_kneeboard = dark_kneeboard + self.theater = theater + + @property + def target_units(self) -> Iterator[Unit]: + if isinstance(self.flight.package.target, TheaterGroundObject): + yield from self.flight.package.target.units + + @staticmethod + def alic_for(unit: Unit) -> str: + try: + return str(AlicCodes.code_for(unit)) + except KeyError: + return "" + + def write(self, path: Path) -> None: + writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard) + if self.flight.custom_name is not None: + custom_name_title = ' ("{}")'.format(self.flight.custom_name) + else: + custom_name_title = "" + task = "DEAD" if self.flight.flight_type == FlightType.DEAD else "SEAD" + writer.title(f"{self.flight.callsign} {task} Target Info{custom_name_title}") + + writer.table( + [self.target_info_row(t) for t in self.target_units], + headers=["Description", "ALIC", "Location"], + ) + + writer.write(path) + + def target_info_row(self, unit: Unit) -> List[str]: + ll = self.theater.point_to_ll(unit.position) + unit_type = unit_type_from_name(unit.type) + name = unit.name if unit_type is None else unit_type.name + return [name, self.alic_for(unit), self.format_ll(ll)] + + +class StrikeTaskPage(KneeboardPage): + """A kneeboard page containing strike target information.""" def __init__( self, flight: FlightData, - targets: List[FlightWaypoint], dark_kneeboard: bool, theater: ConflictTheater, ) -> None: self.flight = flight - self.targets = targets self.dark_kneeboard = dark_kneeboard self.theater = theater + @property + def targets(self) -> Iterator[FlightWaypoint]: + for waypoint in self.flight.waypoints: + if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT: + yield waypoint + def write(self, path: Path) -> None: writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard) if self.flight.custom_name is not None: @@ -447,7 +503,7 @@ class TargetInfoPage(KneeboardPage): def target_info_row(self, target: FlightWaypoint) -> List[str]: ll = self.theater.point_to_ll(target.position) - return [target.pretty_name, f"{ll.latitude} {ll.longitude}"] + return [target.pretty_name, self.format_ll(ll)] class KneeboardGenerator(MissionInfoGenerator): @@ -491,17 +547,12 @@ class KneeboardGenerator(MissionInfoGenerator): ) return all_flights - def generate_target_page(self, flight: FlightData) -> Optional[KneeboardPage]: - target_waypoints = [ - w - for w in flight.waypoints - if w.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC - ] - if not target_waypoints: - return None - return TargetInfoPage( - flight, target_waypoints, self.dark_kneeboard, self.game.theater - ) + def generate_task_page(self, flight: FlightData) -> Optional[KneeboardPage]: + if flight.flight_type in (FlightType.DEAD, FlightType.SEAD): + return SeadTaskPage(flight, self.dark_kneeboard, self.game.theater) + elif flight.flight_type is FlightType.STRIKE: + return StrikeTaskPage(flight, self.dark_kneeboard, self.game.theater) + return None def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]: """Returns a list of kneeboard pages for the given flight.""" @@ -517,7 +568,7 @@ class KneeboardGenerator(MissionInfoGenerator): ), ] - if (target_page := self.generate_target_page(flight)) is not None: + if (target_page := self.generate_task_page(flight)) is not None: pages.append(target_page) return pages