From 8bc77bbf18c61cbb9745ff152ec74aae7f0b8439 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 6 Sep 2020 22:39:59 -0700 Subject: [PATCH 1/3] Make waypoint types less error prone. Make the type of the waypoint a non-optional part of the constructor. Every waypoint needs a type, and there's no good default (the previous default, `TAKEOFF`, is actually unused). All of the target waypoints were mistakenly being set as `TAKEOFF`, so I've fixed that in the process. Also, fix the bug where only the last custom target of a SEAD objective was being added to the waypoint list because the append was scoped incorrectly. --- gen/flights/ai_flight_planner.py | 154 +++++++++++++----- gen/flights/flight.py | 7 +- .../QPredefinedWaypointSelectionComboBox.py | 31 +++- 3 files changed, 143 insertions(+), 49 deletions(-) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 90a91500..42787bfc 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -372,17 +372,26 @@ class FlightPlanner: egress_heading = heading - 180 - 25 ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - ingress_point = FlightWaypoint(ingress_pos.x, ingress_pos.y, self.doctrine["INGRESS_ALT"]) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_STRIKE, + ingress_pos.x, + ingress_pos.y, + self.doctrine["INGRESS_ALT"] + ) ingress_point.pretty_name = "INGRESS on " + location.obj_name ingress_point.description = "INGRESS on " + location.obj_name ingress_point.name = "INGRESS" - ingress_point.waypoint_type = FlightWaypointType.INGRESS_STRIKE flight.points.append(ingress_point) if len(location.groups) > 0 and location.dcs_identifier == "AA": for g in location.groups: for j, u in enumerate(g.units): - point = FlightWaypoint(u.position.x, u.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + u.position.x, + u.position.y, + 0 + ) point.description = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j) point.pretty_name = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j) point.name = location.obj_name + "#" + str(j) @@ -398,7 +407,12 @@ class FlightPlanner: if building.is_dead: continue - point = FlightWaypoint(building.position.x, building.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + building.position.x, + building.position.y, + 0 + ) point.description = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]" point.pretty_name = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]" point.name = building.obj_name @@ -406,7 +420,12 @@ class FlightPlanner: ingress_point.targets.append(building) flight.points.append(point) else: - point = FlightWaypoint(location.position.x, location.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_GROUP_LOC, + location.position.x, + location.position.y, + 0 + ) point.description = "STRIKE on " + location.obj_name point.pretty_name = "STRIKE on " + location.obj_name point.name = location.obj_name @@ -415,11 +434,15 @@ class FlightPlanner: flight.points.append(point) egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - egress_point = FlightWaypoint(egress_pos.x, egress_pos.y, self.doctrine["EGRESS_ALT"]) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress_pos.x, + egress_pos.y, + self.doctrine["EGRESS_ALT"] + ) egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS from " + location.obj_name egress_point.description = "EGRESS from " + location.obj_name - egress_point.waypoint_type = FlightWaypointType.EGRESS flight.points.append(egress_point) descend = self.generate_descend_point(flight.from_cp) @@ -454,18 +477,26 @@ class FlightPlanner: ascend = self.generate_ascend_point(flight.from_cp) flight.points.append(ascend) - orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt) + orbit0 = FlightWaypoint( + FlightWaypointType.PATROL_TRACK, + orbit0p.x, + orbit0p.y, + patrol_alt + ) orbit0.name = "ORBIT 0" orbit0.description = "Standby between this point and the next one" orbit0.pretty_name = "Race-track start" - orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK flight.points.append(orbit0) - orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt) + orbit1 = FlightWaypoint( + FlightWaypointType.PATROL, + orbit1p.x, + orbit1p.y, + patrol_alt + ) orbit1.name = "ORBIT 1" orbit1.description = "Standby between this point and the previous one" orbit1.pretty_name = "Race-track end" - orbit1.waypoint_type = FlightWaypointType.PATROL flight.points.append(orbit1) orbit0.targets.append(for_cp) @@ -512,18 +543,26 @@ class FlightPlanner: ascend = self.generate_ascend_point(flight.from_cp) flight.points.append(ascend) - orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt) + orbit0 = FlightWaypoint( + FlightWaypointType.PATROL_TRACK, + orbit0p.x, + orbit0p.y, + patrol_alt + ) orbit0.name = "ORBIT 0" orbit0.description = "Standby between this point and the next one" orbit0.pretty_name = "Race-track start" - orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK flight.points.append(orbit0) - orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt) + orbit1 = FlightWaypoint( + FlightWaypointType.PATROL, + orbit1p.x, + orbit1p.y, + patrol_alt + ) orbit1.name = "ORBIT 1" orbit1.description = "Standby between this point and the previous one" orbit1.pretty_name = "Race-track end" - orbit1.waypoint_type = FlightWaypointType.PATROL flight.points.append(orbit1) # Note : Targets of a PATROL TRACK waypoints are the points to be defended @@ -555,16 +594,25 @@ class FlightPlanner: egress_heading = heading - 180 - 25 ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - ingress_point = FlightWaypoint(ingress_pos.x, ingress_pos.y, self.doctrine["INGRESS_ALT"]) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_SEAD, + ingress_pos.x, + ingress_pos.y, + self.doctrine["INGRESS_ALT"] + ) ingress_point.name = "INGRESS" ingress_point.pretty_name = "INGRESS on " + location.obj_name ingress_point.description = "INGRESS on " + location.obj_name - ingress_point.waypoint_type = FlightWaypointType.INGRESS_SEAD flight.points.append(ingress_point) if len(custom_targets) > 0: for target in custom_targets: - point = FlightWaypoint(target.position.x, target.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + target.position.x, + target.position.y, + 0 + ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: point.description = "SEAD on " + target.type @@ -574,11 +622,16 @@ class FlightPlanner: point.description = "DEAD on " + location.obj_name point.pretty_name = "DEAD on " + location.obj_name point.only_for_player = True + flight.points.append(point) ingress_point.targets.append(location) ingress_point.targetGroup = location - flight.points.append(point) else: - point = FlightWaypoint(location.position.x, location.position.y, 0) + point = FlightWaypoint( + FlightWaypointType.TARGET_GROUP_LOC, + location.position.x, + location.position.y, + 0 + ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: point.description = "SEAD on " + location.obj_name @@ -593,11 +646,15 @@ class FlightPlanner: flight.points.append(point) egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - egress_point = FlightWaypoint(egress_pos.x, egress_pos.y, self.doctrine["EGRESS_ALT"]) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress_pos.x, + egress_pos.y, + self.doctrine["EGRESS_ALT"] + ) egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS from " + location.obj_name egress_point.description = "EGRESS from " + location.obj_name - egress_point.waypoint_type = FlightWaypointType.EGRESS flight.points.append(egress_point) descend = self.generate_descend_point(flight.from_cp) @@ -628,28 +685,40 @@ class FlightPlanner: ascend.alt = 500 flight.points.append(ascend) - ingress_point = FlightWaypoint(ingress.x, ingress.y, cap_alt) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_CAS, + ingress.x, + ingress.y, + cap_alt + ) ingress_point.alt_type = "RADIO" ingress_point.name = "INGRESS" ingress_point.pretty_name = "INGRESS" ingress_point.description = "Ingress into CAS area" - ingress_point.waypoint_type = FlightWaypointType.INGRESS_CAS flight.points.append(ingress_point) - center_point = FlightWaypoint(center.x, center.y, cap_alt) + center_point = FlightWaypoint( + FlightWaypointType.CAS, + center.x, + center.y, + cap_alt + ) center_point.alt_type = "RADIO" center_point.description = "Provide CAS" center_point.name = "CAS" center_point.pretty_name = "CAS" - center_point.waypoint_type = FlightWaypointType.CAS flight.points.append(center_point) - egress_point = FlightWaypoint(egress.x, egress.y, cap_alt) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress.x, + egress.y, + cap_alt + ) egress_point.alt_type = "RADIO" egress_point.description = "Egress from CAS area" egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS" - egress_point.waypoint_type = FlightWaypointType.EGRESS flight.points.append(egress_point) descend = self.generate_descend_point(flight.from_cp) @@ -660,7 +729,6 @@ class FlightPlanner: rtb = self.generate_rtb_waypoint(flight.from_cp) flight.points.append(rtb) - def generate_ascend_point(self, from_cp): """ Generate ascend point @@ -669,15 +737,18 @@ class FlightPlanner: """ ascend_heading = from_cp.heading pos_ascend = from_cp.position.point_from_heading(ascend_heading, 10000) - ascend = FlightWaypoint(pos_ascend.x, pos_ascend.y, self.doctrine["PATTERN_ALTITUDE"]) + ascend = FlightWaypoint( + FlightWaypointType.ASCEND_POINT, + pos_ascend.x, + pos_ascend.y, + self.doctrine["PATTERN_ALTITUDE"] + ) ascend.name = "ASCEND" ascend.alt_type = "RADIO" ascend.description = "Ascend" ascend.pretty_name = "Ascend" - ascend.waypoint_type = FlightWaypointType.ASCEND_POINT return ascend - def generate_descend_point(self, from_cp): """ Generate approach/descend point @@ -686,15 +757,18 @@ class FlightPlanner: """ ascend_heading = from_cp.heading descend = from_cp.position.point_from_heading(ascend_heading - 180, 10000) - descend = FlightWaypoint(descend.x, descend.y, self.doctrine["PATTERN_ALTITUDE"]) + descend = FlightWaypoint( + FlightWaypointType.DESCENT_POINT, + descend.x, + descend.y, + self.doctrine["PATTERN_ALTITUDE"] + ) descend.name = "DESCEND" descend.alt_type = "RADIO" descend.description = "Descend to pattern alt" descend.pretty_name = "Descend to pattern alt" - descend.waypoint_type = FlightWaypointType.DESCENT_POINT return descend - def generate_rtb_waypoint(self, from_cp): """ Generate RTB landing point @@ -702,10 +776,14 @@ class FlightPlanner: :return: """ rtb = from_cp.position - rtb = FlightWaypoint(rtb.x, rtb.y, 0) + rtb = FlightWaypoint( + FlightWaypointType.LANDING_POINT, + rtb.x, + rtb.y, + 0 + ) rtb.name = "LANDING" rtb.alt_type = "RADIO" rtb.description = "RTB" rtb.pretty_name = "RTB" - rtb.waypoint_type = FlightWaypointType.LANDING_POINT - return rtb \ No newline at end of file + return rtb diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 6e2522f5..b736247f 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -60,7 +60,9 @@ class PredefinedWaypointCategory(Enum): class FlightWaypoint: - def __init__(self, x: float, y: float, alt=0): + def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float, + alt: int = 0) -> None: + self.waypoint_type = waypoint_type self.x = x self.y = y self.alt = alt @@ -71,8 +73,7 @@ class FlightWaypoint: self.targetGroup = None self.obj_name = "" self.pretty_name = "" - self.waypoint_type = FlightWaypointType.TAKEOFF # type: FlightWaypointType - self.category = PredefinedWaypointCategory.NOT_PREDEFINED# type: PredefinedWaypointCategory + self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED self.only_for_player = False self.data = None diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index c8721a17..079aab99 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -57,13 +57,16 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): enemy_cp = [ecp for ecp in cp.connected_points if ecp.captured != cp.captured] for ecp in enemy_cp: pos = Conflict.frontline_position(self.game.theater, cp, ecp)[0] - wpt = FlightWaypoint(pos.x, pos.y, 800) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + pos.x, + pos.y, + 800) wpt.name = "Frontline " + cp.name + "/" + ecp.name + " [CAS]" wpt.alt_type = "RADIO" wpt.pretty_name = wpt.name wpt.description = "Frontline" wpt.data = [cp, ecp] - wpt.waypoint_type = FlightWaypointType.CUSTOM wpt.category = PredefinedWaypointCategory.FRONTLINE i = add_model_item(i, model, wpt.pretty_name, wpt) @@ -72,14 +75,18 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured): for ground_object in cp.ground_objects: if not ground_object.is_dead and not ground_object.dcs_identifier == "AA": - wpt = FlightWaypoint(ground_object.position.x,ground_object.position.y, 0) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + ground_object.position.x, + ground_object.position.y, + 0 + ) wpt.alt_type = "RADIO" wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + ground_object.category + " #" + str(ground_object.object_id) wpt.pretty_name = wpt.name wpt.obj_name = ground_object.obj_name wpt.targets.append(ground_object) wpt.data = ground_object - wpt.waypoint_type = FlightWaypointType.CUSTOM if cp.captured: wpt.description = "Friendly Building" wpt.category = PredefinedWaypointCategory.ALLY_BUILDING @@ -95,7 +102,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): if not ground_object.is_dead and ground_object.dcs_identifier == "AA": for g in ground_object.groups: for j, u in enumerate(g.units): - wpt = FlightWaypoint(u.position.x, u.position.y, 0) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + u.position.x, + u.position.y, + 0 + ) wpt.alt_type = "RADIO" wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j) wpt.pretty_name = wpt.name @@ -114,11 +126,15 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): if self.include_airbases: for cp in self.game.theater.controlpoints: if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured): - wpt = FlightWaypoint(cp.position.x, cp.position.y, 0) + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + cp.position.x, + cp.position.y, + 0 + ) wpt.alt_type = "RADIO" wpt.name = cp.name wpt.data = cp - wpt.waypoint_type = FlightWaypointType.CUSTOM if cp.captured: wpt.description = "Position of " + cp.name + " [Friendly Airbase]" wpt.category = PredefinedWaypointCategory.ALLY_CP @@ -133,7 +149,6 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): else: wpt.pretty_name = cp.name + " (Airbase)" - i = add_model_item(i, model, wpt.pretty_name, wpt) self.setModel(model) From 180537cd4830f213bb1e0d3b91560d1c37d64764 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 6 Sep 2020 22:50:30 -0700 Subject: [PATCH 2/3] Fix SEAD/DEAD reversal. --- gen/flights/ai_flight_planner.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 42787bfc..99cf8427 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -615,12 +615,12 @@ class FlightPlanner: ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: - point.description = "SEAD on " + target.type - point.pretty_name = "SEAD on " + location.obj_name + point.description = "DEAD on " + target.type + point.pretty_name = "DEAD on " + location.obj_name point.only_for_player = True else: - point.description = "DEAD on " + location.obj_name - point.pretty_name = "DEAD on " + location.obj_name + point.description = "SEAD on " + location.obj_name + point.pretty_name = "SEAD on " + location.obj_name point.only_for_player = True flight.points.append(point) ingress_point.targets.append(location) @@ -634,13 +634,13 @@ class FlightPlanner: ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: - point.description = "SEAD on " + location.obj_name - point.pretty_name = "SEAD on " + location.obj_name - point.only_for_player = True - else: point.description = "DEAD on " + location.obj_name point.pretty_name = "DEAD on " + location.obj_name point.only_for_player = True + else: + point.description = "SEAD on " + location.obj_name + point.pretty_name = "SEAD on " + location.obj_name + point.only_for_player = True ingress_point.targets.append(location) ingress_point.targetGroup = location flight.points.append(point) From d4820b2435c6573ad643a2aea380150f719146bb Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 10 Sep 2020 01:33:54 -0700 Subject: [PATCH 3/3] Coalesce large runs of target waypoints. Since we create a target waypoint for every target in a strike/SEAD/DEAD objective area (including every ground vehicle), the kneeboard can quickly be overrun with target waypoints. When there are many target waypoints, collapse them all into a single row for brevity. --- gen/kneeboard.py | 62 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 4bc5d64d..d96dc466 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -23,19 +23,21 @@ only be added per airframe, so PvP missions where each side have the same aircraft will be able to see the enemy's kneeboard for the same airframe. """ from collections import defaultdict +from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional, Tuple from PIL import Image, ImageDraw, ImageFont -from tabulate import tabulate - from dcs.mission import Mission from dcs.unittype import FlyingType +from tabulate import tabulate + from . import units from .aircraft import FlightData from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator +from .flights.flight import FlightWaypoint, FlightWaypointType from .radios import RadioFrequency @@ -95,6 +97,54 @@ class KneeboardPage: raise NotImplementedError +@dataclass(frozen=True) +class NumberedWaypoint: + number: int + waypoint: FlightWaypoint + + +class FlightPlanBuilder: + def __init__(self) -> None: + self.rows: List[List[str]] = [] + self.target_points: List[NumberedWaypoint] = [] + + def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None: + if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT: + self.target_points.append(NumberedWaypoint(waypoint_num, waypoint)) + return + + if self.target_points: + self.coalesce_target_points() + self.target_points = [] + + self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint)) + + def coalesce_target_points(self) -> None: + if len(self.target_points) <= 4: + for steerpoint in self.target_points: + self.add_waypoint_row(steerpoint) + return + + first_waypoint_num = self.target_points[0].number + last_waypoint_num = self.target_points[-1].number + + self.rows.append([ + f"{first_waypoint_num}-{last_waypoint_num}", + "Target points", + "0" + ]) + + def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None: + self.rows.append([ + waypoint.number, + waypoint.waypoint.pretty_name, + str(int(units.meters_to_feet(waypoint.waypoint.alt))) + ]) + + def build(self) -> List[List[str]]: + return self.rows + + class BriefingPage(KneeboardPage): """A kneeboard page containing briefing information.""" def __init__(self, flight: FlightData, comms: List[CommInfo], @@ -122,11 +172,11 @@ class BriefingPage(KneeboardPage): ], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"]) writer.heading("Flight Plan") - flight_plan = [] + flight_plan_builder = FlightPlanBuilder() for num, waypoint in enumerate(self.flight.waypoints): - alt = int(units.meters_to_feet(waypoint.alt)) - flight_plan.append([num, waypoint.pretty_name, str(alt)]) - writer.table(flight_plan, headers=["STPT", "Action", "Alt"]) + flight_plan_builder.add_waypoint(num, waypoint) + writer.table(flight_plan_builder.build(), + headers=["STPT", "Action", "Alt"]) writer.heading("Comm Ladder") comms = []