From 85f66161854ee4c0562eccb2da3b8bf3677b0aa6 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 12 Oct 2020 22:03:43 -0700 Subject: [PATCH 01/10] Add bombers to coalitions. * B-1 * B-52 * F-117 * Tu-160 * Tu-22 * Tu-95 Also alters the default loadouts for the F-15E. --- game/db.py | 107 +++++++++++++++++--------- game/factions/bluefor_coldwar.py | 3 + game/factions/bluefor_coldwar_a4.py | 3 + game/factions/bluefor_coldwar_mods.py | 3 + game/factions/bluefor_modern.py | 7 ++ game/factions/russia_1955.py | 12 ++- game/factions/russia_1965.py | 3 + game/factions/russia_1975.py | 5 ++ game/factions/russia_1990.py | 7 ++ game/factions/russia_2010.py | 7 ++ game/factions/russia_2020.py | 7 ++ game/factions/us_aggressors.py | 7 ++ game/factions/usa_1955.py | 3 + game/factions/usa_1960.py | 3 + game/factions/usa_1990.py | 7 ++ game/factions/usa_2005.py | 7 ++ gen/aircraft.py | 1 - gen/flights/ai_flight_planner_db.py | 27 ++++++- 18 files changed, 179 insertions(+), 40 deletions(-) diff --git a/game/db.py b/game/db.py index 432d23dc..bc221df2 100644 --- a/game/db.py +++ b/game/db.py @@ -43,6 +43,7 @@ from dcs.planes import ( FA_18C_hornet, FW_190A8, FW_190D9, + F_117A, F_14B, F_15C, F_15E, @@ -97,6 +98,9 @@ from dcs.planes import ( Su_34, Tornado_GR4, Tornado_IDS, + Tu_160, + Tu_22M3, + Tu_95MS, WingLoong_I, Yak_40, plane_map, @@ -365,6 +369,10 @@ PRICES = { # Bombers B_52H: 35, B_1B: 50, + F_117A: 100, + Tu_160: 50, + Tu_22M3: 40, + Tu_95MS: 35, # special IL_76MD: 30, @@ -645,49 +653,53 @@ UNIT_BY_TASK = { SA342Mistral ], CAS: [ - F_15E, - F_86F_Sabre, - MiG_15bis, - L_39ZA, - AV8BNA, + AH_1W, + AH_64A, + AH_64D, AJS37, + AV8BNA, A_10A, A_10C, A_10C_2, - Su_17M4, - Su_25, - Su_25T, - Su_34, - Ka_50, - SA342M, - SA342L, - SA342Minigun, - Su_24M, - Su_24MR, - AH_64A, - AH_64D, - OH_58D, - B_52H, - B_1B, - Tornado_IDS, - Tornado_GR4, - UH_1H, - Mi_8MT, - Mi_28N, - Mi_24V, - MiG_27K, A_20G, + B_17G, + B_1B, + B_52H, + F_117A, + F_15E, + F_86F_Sabre, + Ju_88A4, + Ka_50, + L_39ZA, + MB_339PAN, + MQ_9_Reaper, + MiG_15bis, + MiG_27K, + Mi_24V, + Mi_28N, + Mi_8MT, + OH_58D, P_47D_30, P_47D_30bl1, P_47D_40, - Ju_88A4, - B_17G, - MB_339PAN, - Rafale_A_S, - WingLoong_I, - MQ_9_Reaper, RQ_1A_Predator, - AH_1W + Rafale_A_S, + SA342L, + SA342M, + SA342Minigun, + Su_17M4, + Su_24M, + Su_24MR, + Su_25, + Su_25T, + Su_34, + Tornado_GR4, + Tornado_IDS, + Tu_160, + Tu_22M3, + Tu_95MS, + UH_1H, + WingLoong_I, ], Transport: [ IL_76MD, @@ -1095,6 +1107,23 @@ COMMON_OVERRIDE = { PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { + B_1B: { + CAS: "GBU-38*16, CBU-97*20", + PinpointStrike: "GBU-31*8, GBU-38*32", + GroundAttack: "GBU-31*8, GBU-38*32", + }, + B_52H: { + PinpointStrike: "AGM-86C*20", + GroundAttack: "Mk 82*51", + }, + F_117A: { + PinpointStrike: "GBU-10*2", + }, + F_15E: { + CAS: "AIM-120B*2,AIM-9M*2,FUEL,GBU-12*4,GBU-38*4,AGM-65D*2", + GroundAttack: "AIM-120B*2,AIM-9M*2,FUEL*3,CBU-97*12", + PinpointStrike: "AIM-120B*2,AIM-9M*2,FUEL,GBU-31*4,AGM-154C*2", + }, FA_18C_hornet: { CAP: "CAP HEAVY", Intercept: "CAP HEAVY", @@ -1115,6 +1144,15 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { GroundAttack: "STRIKE", Escort: "CAP HEAVY", }, + Tu_160: { + PinpointStrike: "Kh-65*12", + }, + Tu_22M3: { + GroundAttack: "FAB-500*33, FAB-250*36", + }, + Tu_95MS: { + PinpointStrike: "Kh-65*6", + }, A_10A: COMMON_OVERRIDE, A_10C: COMMON_OVERRIDE, A_10C_2: COMMON_OVERRIDE, @@ -1123,7 +1161,6 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { F_5E_3: COMMON_OVERRIDE, F_14B: COMMON_OVERRIDE, F_15C: COMMON_OVERRIDE, - F_15E: COMMON_OVERRIDE, F_16C_50: COMMON_OVERRIDE, JF_17: COMMON_OVERRIDE, M_2000C: COMMON_OVERRIDE, diff --git a/game/factions/bluefor_coldwar.py b/game/factions/bluefor_coldwar.py index c241bbae..b2a4a494 100644 --- a/game/factions/bluefor_coldwar.py +++ b/game/factions/bluefor_coldwar.py @@ -6,6 +6,7 @@ from dcs.helicopters import ( from dcs.planes import ( AJS37, A_10A, + B_52H, C_130, E_3A, F_14B, @@ -37,6 +38,8 @@ BLUEFOR_COLDWAR = { A_10A, AJS37, + B_52H, + KC_135, KC130, C_130, diff --git a/game/factions/bluefor_coldwar_a4.py b/game/factions/bluefor_coldwar_a4.py index ce6cf016..1244ffbd 100644 --- a/game/factions/bluefor_coldwar_a4.py +++ b/game/factions/bluefor_coldwar_a4.py @@ -6,6 +6,7 @@ from dcs.helicopters import ( from dcs.planes import ( AJS37, A_10A, + B_52H, C_130, E_3A, F_14B, @@ -41,6 +42,8 @@ BLUEFOR_COLDWAR_A4 = { AJS37, A_4E_C, + B_52H, + KC_135, KC130, C_130, diff --git a/game/factions/bluefor_coldwar_mods.py b/game/factions/bluefor_coldwar_mods.py index a395fc48..2eed5f31 100644 --- a/game/factions/bluefor_coldwar_mods.py +++ b/game/factions/bluefor_coldwar_mods.py @@ -6,6 +6,7 @@ from dcs.helicopters import ( from dcs.planes import ( AJS37, A_10A, + B_52H, C_130, E_3A, F_14B, @@ -43,6 +44,8 @@ BLUEFOR_COLDWAR_MODS = { A_4E_C, MB_339PAN, + B_52H, + KC_135, KC130, C_130, diff --git a/game/factions/bluefor_modern.py b/game/factions/bluefor_modern.py index 9f97827b..0798f02f 100644 --- a/game/factions/bluefor_modern.py +++ b/game/factions/bluefor_modern.py @@ -11,11 +11,14 @@ from dcs.planes import ( A_10A, A_10C, A_10C_2, + B_1B, + B_52H, C_130, E_3A, FA_18C_hornet, F_14B, F_15C, + F_15E, F_16C_50, F_5E_3, JF_17, @@ -47,6 +50,7 @@ BLUEFOR_MODERN = { "units": [ F_15C, + F_15E, F_14B, FA_18C_hornet, F_16C_50, @@ -62,6 +66,9 @@ BLUEFOR_MODERN = { AV8BNA, AJS37, + B_1B, + B_52H, + KC_135, KC130, C_130, diff --git a/game/factions/russia_1955.py b/game/factions/russia_1955.py index 5730bd9d..98624ad5 100644 --- a/game/factions/russia_1955.py +++ b/game/factions/russia_1955.py @@ -1,4 +1,12 @@ -from dcs.planes import An_26B, An_30M, IL_76MD, IL_78M, MiG_15bis, Yak_40 +from dcs.planes import ( + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_15bis, + Tu_95MS, + Yak_40, +) from dcs.ships import ( Bulk_cargo_ship_Yakushev, CV_1143_5_Admiral_Kuznetsov, @@ -19,6 +27,8 @@ Russia_1955 = { An_30M, Yak_40, + Tu_95MS, + AirDefence.AAA_ZU_23_Closed, AirDefence.AAA_ZU_23_on_Ural_375, Armor.ARV_BRDM_2, diff --git a/game/factions/russia_1965.py b/game/factions/russia_1965.py index 9d88d251..cb5d298a 100644 --- a/game/factions/russia_1965.py +++ b/game/factions/russia_1965.py @@ -8,6 +8,7 @@ from dcs.planes import ( MiG_15bis, MiG_19P, MiG_21Bis, + Tu_95MS, Yak_40, ) from dcs.ships import ( @@ -32,6 +33,8 @@ Russia_1965 = { An_30M, Yak_40, + Tu_95MS, + A_50, Mi_8MT, diff --git a/game/factions/russia_1975.py b/game/factions/russia_1975.py index b8a75437..b12d28d4 100644 --- a/game/factions/russia_1975.py +++ b/game/factions/russia_1975.py @@ -15,6 +15,8 @@ from dcs.planes import ( Su_17M4, Su_24M, Su_25, + Tu_22M3, + Tu_95MS, Yak_40, ) from dcs.ships import ( @@ -41,6 +43,9 @@ Russia_1975 = { Su_24M, Su_25, + Tu_22M3, + Tu_95MS, + IL_76MD, IL_78M, An_26B, diff --git a/game/factions/russia_1990.py b/game/factions/russia_1990.py index 747024a8..71e4c494 100644 --- a/game/factions/russia_1990.py +++ b/game/factions/russia_1990.py @@ -17,6 +17,9 @@ from dcs.planes import ( Su_24M, Su_25, Su_27, + Tu_160, + Tu_22M3, + Tu_95MS, Yak_40, ) from dcs.ships import ( @@ -51,6 +54,10 @@ Russia_1990 = { Su_25, Ka_50, + Tu_160, + Tu_22M3, + Tu_95MS, + IL_76MD, IL_78M, An_26B, diff --git a/game/factions/russia_2010.py b/game/factions/russia_2010.py index 13adefb6..852871f6 100644 --- a/game/factions/russia_2010.py +++ b/game/factions/russia_2010.py @@ -20,6 +20,9 @@ from dcs.planes import ( Su_30, Su_33, Su_34, + Tu_160, + Tu_22M3, + Tu_95MS, Yak_40, ) from dcs.ships import ( @@ -55,6 +58,10 @@ Russia_2010 = { Su_24M, L_39ZA, + Tu_160, + Tu_22M3, + Tu_95MS, + IL_76MD, IL_78M, An_26B, diff --git a/game/factions/russia_2020.py b/game/factions/russia_2020.py index 6cc60bc1..5df17da7 100644 --- a/game/factions/russia_2020.py +++ b/game/factions/russia_2020.py @@ -20,6 +20,9 @@ from dcs.planes import ( Su_30, Su_33, Su_34, + Tu_160, + Tu_22M3, + Tu_95MS, Yak_40, ) from dcs.ships import ( @@ -58,6 +61,10 @@ Russia_2020 = { Su_24M, L_39ZA, + Tu_160, + Tu_22M3, + Tu_95MS, + IL_76MD, IL_78M, An_26B, diff --git a/game/factions/us_aggressors.py b/game/factions/us_aggressors.py index 650c09cd..4e2cce11 100644 --- a/game/factions/us_aggressors.py +++ b/game/factions/us_aggressors.py @@ -6,10 +6,13 @@ from dcs.helicopters import ( UH_1H, ) from dcs.planes import ( + B_1B, + B_52H, C_130, E_3A, FA_18C_hornet, F_15C, + F_15E, F_16C_50, F_5E_3, KC130, @@ -38,11 +41,15 @@ US_Aggressors = { "units": [ F_15C, + F_15E, F_5E_3, FA_18C_hornet, F_16C_50, Su_27, + B_1B, + B_52H, + KC_135, KC130, C_130, diff --git a/game/factions/usa_1955.py b/game/factions/usa_1955.py index 1943544b..bbafc9c4 100644 --- a/game/factions/usa_1955.py +++ b/game/factions/usa_1955.py @@ -1,4 +1,5 @@ from dcs.planes import ( + B_52H, C_130, E_3A, F_86F_Sabre, @@ -25,6 +26,8 @@ USA_1955 = { F_86F_Sabre, P_51D, + B_52H, + KC_135, KC130, C_130, diff --git a/game/factions/usa_1960.py b/game/factions/usa_1960.py index ee162d04..b87ec470 100644 --- a/game/factions/usa_1960.py +++ b/game/factions/usa_1960.py @@ -2,6 +2,7 @@ from dcs.helicopters import ( UH_1H, ) from dcs.planes import ( + B_52H, C_130, E_3A, F_86F_Sabre, @@ -28,6 +29,8 @@ USA_1960 = { F_86F_Sabre, P_51D, + B_52H, + KC_135, KC130, C_130, diff --git a/game/factions/usa_1990.py b/game/factions/usa_1990.py index 4014237a..7413d956 100644 --- a/game/factions/usa_1990.py +++ b/game/factions/usa_1990.py @@ -5,9 +5,12 @@ from dcs.helicopters import ( from dcs.planes import ( AV8BNA, A_10A, + B_1B, + B_52H, C_130, E_3A, FA_18C_hornet, + F_117A, F_14B, F_15C, F_15E, @@ -43,6 +46,10 @@ USA_1990 = { A_10A, AV8BNA, + B_1B, + B_52H, + F_117A, + KC_135, KC130, C_130, diff --git a/game/factions/usa_2005.py b/game/factions/usa_2005.py index 5b9e03ca..5a40aa8b 100644 --- a/game/factions/usa_2005.py +++ b/game/factions/usa_2005.py @@ -6,9 +6,12 @@ from dcs.planes import ( AV8BNA, A_10C, A_10C_2, + B_1B, + B_52H, C_130, E_3A, FA_18C_hornet, + F_117A, F_14B, F_15C, F_15E, @@ -46,6 +49,10 @@ USA_2005 = { AV8BNA, MQ_9_Reaper, + B_1B, + B_52H, + F_117A, + KC_135, KC130, C_130, diff --git a/gen/aircraft.py b/gen/aircraft.py index 0c4aa97c..0a8c1cf7 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -579,7 +579,6 @@ class AircraftConflictGenerator: unit_type = group.units[0].unit_type if unit_type in db.PLANE_PAYLOAD_OVERRIDES: - override_loadout = db.PLANE_PAYLOAD_OVERRIDES[unit_type] # Clear pylons for p in group.units: p.pylons.clear() diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index 02b21f40..fbc2b257 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -19,11 +19,14 @@ from dcs.planes import ( A_10C_2, A_20G, B_17G, + B_1B, + B_52H, Bf_109K_4, C_101CC, FA_18C_hornet, FW_190A8, FW_190D9, + F_117A, F_14B, F_15C, F_15E, @@ -71,6 +74,9 @@ from dcs.planes import ( Su_34, Tornado_GR4, Tornado_IDS, + Tu_160, + Tu_22M3, + Tu_95MS, WingLoong_I, ) @@ -226,6 +232,8 @@ CAS_CAPABLE = [ F_16C_50, FA_18C_hornet, + B_1B, + Tornado_IDS, Tornado_GR4, @@ -367,6 +375,10 @@ STRIKE_CAPABLE = [ Su_25T, Su_34, + Tu_160, + Tu_22M3, + Tu_95MS, + JF_17, M_2000C, @@ -384,6 +396,10 @@ STRIKE_CAPABLE = [ F_16C_50, FA_18C_hornet, + B_1B, + B_52H, + F_117A, + Tornado_IDS, Tornado_GR4, @@ -413,11 +429,16 @@ STRIKE_CAPABLE = [ STRIKE_PREFERRED = [ AJS37, - F_15E, - Tornado_GR4, - A_20G, B_17G, + B_1B, + B_52H, + F_117A, + F_15E, + Tornado_GR4, + Tu_160, + Tu_22M3, + Tu_95MS, ] ANTISHIP_CAPABLE = [ From aa7ffdabb0f1475a09c42ba5d90d5a445afd3f8b Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 23 Oct 2020 14:19:12 -0700 Subject: [PATCH 02/10] Fix planned flights view, stop hiding bugs. --- .../basemenu/airfield/QAirfieldCommand.py | 18 +++++++----------- qt_ui/windows/mission/QPlannedFlightsView.py | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py index 5274640d..9965115a 100644 --- a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py +++ b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py @@ -19,16 +19,12 @@ class QAirfieldCommand(QFrame): layout = QGridLayout() layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0) - try: - planned = QGroupBox("Planned Flights") - planned_layout = QVBoxLayout() - planned_layout.addWidget( - QPlannedFlightsView(self.game_model, self.cp) - ) - planned.setLayout(planned_layout) - layout.addWidget(planned, 0, 1) - except: - pass + planned = QGroupBox("Planned Flights") + planned_layout = QVBoxLayout() + planned_layout.addWidget( + QPlannedFlightsView(self.game_model, self.cp) + ) + planned.setLayout(planned_layout) + layout.addWidget(planned, 0, 1) - #layout.addWidget(QAirportInformation(self.cp, self.game.theater.terrain.airport_by_id(self.cp.id)), 0, 2) self.setLayout(layout) diff --git a/qt_ui/windows/mission/QPlannedFlightsView.py b/qt_ui/windows/mission/QPlannedFlightsView.py index a7c45e51..2c602d56 100644 --- a/qt_ui/windows/mission/QPlannedFlightsView.py +++ b/qt_ui/windows/mission/QPlannedFlightsView.py @@ -25,7 +25,7 @@ class QPlannedFlightsView(QListView): for package in self.game_model.ato_model.packages: for flight in package.flights: if flight.from_cp == self.cp: - item = QFlightItem(flight) + item = QFlightItem(package.package, flight) self.model.appendRow(item) self.flight_items.append(item) self.set_selected_flight(0) From dd4c37cde331931cf77cee47b21e63abf9a4c9ef Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 17 Oct 2020 16:36:49 -0700 Subject: [PATCH 03/10] Pick runways and ascent/descent based on headwind. --- gen/aircraft.py | 26 ++---- gen/airfields.py | 66 +--------------- gen/briefinggen.py | 7 +- gen/flights/flightplan.py | 18 ++--- gen/flights/waypointbuilder.py | 14 ++-- gen/groundobjectsgen.py | 6 +- gen/kneeboard.py | 6 +- gen/runways.py | 139 +++++++++++++++++++++++++++++++++ 8 files changed, 177 insertions(+), 105 deletions(-) create mode 100644 gen/runways.py diff --git a/gen/aircraft.py b/gen/aircraft.py index 0a8c1cf7..7e2ceb1a 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -64,7 +64,6 @@ from game import db from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter -from gen.airfields import RunwayData from gen.airsupportgen import AirSupport from gen.ato import AirTaskingOrder, Package from gen.callsigns import create_group_callsign_from_unit @@ -75,11 +74,13 @@ from gen.flights.flight import ( FlightWaypointType, ) from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio +from gen.runways import RunwayData from theater import TheaterGroundObject from theater.controlpoint import ControlPoint, ControlPointType from .conflictgen import Conflict from .flights.traveltime import PackageWaypointTiming, TotEstimator from .naming import namegen +from .runways import RunwayAssigner WARM_START_HELI_AIRSPEED = 120 WARM_START_HELI_ALT = 500 @@ -621,9 +622,12 @@ class AircraftConflictGenerator: # TODO: Support for different departure/arrival airfields. cp = flight.from_cp - fallback_runway = RunwayData(cp.full_name, runway_name="") + fallback_runway = RunwayData(cp.full_name, runway_heading=0, + runway_name="") if cp.cptype == ControlPointType.AIRBASE: - departure_runway = self.get_preferred_runway(flight.from_cp.airport) + assigner = RunwayAssigner(self.game.conditions) + departure_runway = assigner.get_preferred_runway( + flight.from_cp.airport) elif cp.is_fleet: departure_runway = dynamic_runways.get(cp.name, fallback_runway) else: @@ -655,22 +659,6 @@ class AircraftConflictGenerator: for unit in group.units: unit.fuel = Su_33.fuel_max * 0.8 - def get_preferred_runway(self, airport: Airport) -> RunwayData: - """Returns the preferred runway for the given airport. - - Right now we're only selecting runways based on whether or not they have - ILS, but we could also choose based on wind conditions, or which - direction flight plans should follow. - """ - runways = list(RunwayData.for_pydcs_airport(airport)) - for runway in runways: - # Prefer any runway with ILS. - if runway.ils is not None: - return runway - # Otherwise we lack the mission information to pick more usefully, - # so just use the first runway. - return runways[0] - def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, start_type: str, airport: Optional[Airport] = None) -> FlyingGroup: diff --git a/gen/airfields.py b/gen/airfields.py index b3185158..5ea5c57c 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -3,11 +3,11 @@ Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing data added to pydcs. Until then, missing data can be manually filled in here. """ -from dataclasses import dataclass, field -import logging -from typing import Dict, Iterator, Optional, Tuple +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Optional, Tuple -from dcs.terrain.terrain import Airport from .radios import MHz, RadioFrequency from .tacan import TacanBand, TacanChannel @@ -1503,61 +1503,3 @@ AIRFIELD_DATA = { atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), ), } - - -@dataclass(frozen=True) -class RunwayData: - airfield_name: str - runway_name: str - atc: Optional[RadioFrequency] = None - tacan: Optional[TacanChannel] = None - tacan_callsign: Optional[str] = None - ils: Optional[RadioFrequency] = None - icls: Optional[int] = None - - @classmethod - def for_airfield(cls, airport: Airport, runway: str) -> "RunwayData": - """Creates RunwayData for the given runway of an airfield. - - Args: - airport: The airfield the runway belongs to. - runway: Identifier of the runway to use. e.g. "03" or "20L". - """ - atc: Optional[RadioFrequency] = None - tacan: Optional[TacanChannel] = None - tacan_callsign: Optional[str] = None - ils: Optional[RadioFrequency] = None - try: - airfield = AIRFIELD_DATA[airport.name] - if airfield.atc is not None: - atc = airfield.atc.uhf - else: - atc = None - tacan = airfield.tacan - tacan_callsign = airfield.tacan_callsign - ils = airfield.ils_freq(runway) - except KeyError: - logging.warning(f"No airfield data for {airport.name}") - return cls( - airfield_name=airport.name, - runway_name=runway, - atc=atc, - tacan=tacan, - tacan_callsign=tacan_callsign, - ils=ils - ) - - @classmethod - def for_pydcs_airport(cls, airport: Airport) -> Iterator["RunwayData"]: - for runway in airport.runways: - runway_number = runway.heading // 10 - runway_side = ["", "L", "R"][runway.leftright] - runway_name = f"{runway_number:02}{runway_side}" - yield cls.for_airfield(airport, runway_name) - - # pydcs only exposes one runway per physical runway, so to expose - # both sides of the runway we need to generate the other. - runway_number = ((runway.heading + 180) % 360) // 10 - runway_side = ["", "R", "L"][runway.leftright] - runway_name = f"{runway_number:02}{runway_side}" - yield cls.for_airfield(airport, runway_name) diff --git a/gen/briefinggen.py b/gen/briefinggen.py index 1eef67a7..63f29396 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -1,19 +1,20 @@ import datetime import os +import random from collections import defaultdict from dataclasses import dataclass -import random from typing import List -from game import db from dcs.mission import Mission + +from game import db from .aircraft import FlightData -from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo from .armor import JtacInfo from .conflictgen import Conflict from .ground_forces.combat_stance import CombatStance from .radios import RadioFrequency +from .runways import RunwayData @dataclass diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index e89d2f53..7c9f26f1 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -132,7 +132,7 @@ class FlightPlanBuilder: if not isinstance(location, TheaterGroundObject): raise InvalidObjectiveLocation(flight.flight_type, location) - builder = WaypointBuilder(flight, self.doctrine) + builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder.ascent(flight.from_cp) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) @@ -222,7 +222,7 @@ class FlightPlanBuilder: ) start = end.point_from_heading(heading - 180, diameter) - builder = WaypointBuilder(flight, self.doctrine) + builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder.ascent(flight.from_cp) builder.race_track(start, end, patrol_alt) builder.rtb(flight.from_cp) @@ -264,7 +264,7 @@ class FlightPlanBuilder: orbit1p = orbit_center.point_from_heading(heading + 180, radius) # Create points - builder = WaypointBuilder(flight, self.doctrine) + builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder.ascent(flight.from_cp) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) @@ -290,7 +290,7 @@ class FlightPlanBuilder: if custom_targets is None: custom_targets = [] - builder = WaypointBuilder(flight, self.doctrine) + builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder.ascent(flight.from_cp) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) @@ -328,7 +328,7 @@ class FlightPlanBuilder: def generate_escort(self, flight: Flight) -> None: assert self.package.waypoints is not None - builder = WaypointBuilder(flight, self.doctrine) + builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder.ascent(flight.from_cp) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) @@ -361,7 +361,7 @@ class FlightPlanBuilder: center = ingress.point_from_heading(heading, distance / 2) egress = ingress.point_from_heading(heading, distance) - builder = WaypointBuilder(flight, self.doctrine) + builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder.ascent(flight.from_cp, is_helo) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) @@ -382,7 +382,7 @@ class FlightPlanBuilder: flight: The flight to generate the descend point for. departure: Departure airfield or carrier. """ - builder = WaypointBuilder(flight, self.doctrine) + builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder.ascent(departure) return builder.build()[0] @@ -394,7 +394,7 @@ class FlightPlanBuilder: flight: The flight to generate the descend point for. arrival: Arrival airfield or carrier. """ - builder = WaypointBuilder(flight, self.doctrine) + builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder.descent(arrival) return builder.build()[0] @@ -406,7 +406,7 @@ class FlightPlanBuilder: flight: The flight to generate the landing waypoint for. arrival: Arrival airfield or carrier. """ - builder = WaypointBuilder(flight, self.doctrine) + builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder.land(arrival) return builder.build()[0] diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 5ee8820c..cdaefd0b 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -7,12 +7,16 @@ from dcs.unit import Unit from game.data.doctrine import Doctrine from game.utils import nm_to_meter +from game.weather import Conditions from theater import ControlPoint, MissionTarget, TheaterGroundObject from .flight import Flight, FlightWaypoint, FlightWaypointType +from ..runways import RunwayAssigner class WaypointBuilder: - def __init__(self, flight: Flight, doctrine: Doctrine) -> None: + def __init__(self, conditions: Conditions, flight: Flight, + doctrine: Doctrine) -> None: + self.conditions = conditions self.flight = flight self.doctrine = doctrine self.waypoints: List[FlightWaypoint] = [] @@ -28,8 +32,7 @@ class WaypointBuilder: departure: Departure airfield or carrier. is_helo: True if the flight is a helicopter. """ - # TODO: Pick runway based on wind direction. - heading = departure.heading + heading = RunwayAssigner(self.conditions).takeoff_heading(departure) position = departure.position.point_from_heading( heading, nm_to_meter(5) ) @@ -52,9 +55,8 @@ class WaypointBuilder: arrival: Arrival airfield or carrier. is_helo: True if the flight is a helicopter. """ - # TODO: Pick runway based on wind direction. - # ControlPoint.heading is the departure heading. - heading = (arrival.heading + 180) % 360 + landing_heading = RunwayAssigner(self.conditions).landing_heading(arrival) + heading = (landing_heading + 180) % 360 position = arrival.position.point_from_heading( heading, nm_to_meter(5) ) diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 2d1894e8..779492e0 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -16,9 +16,9 @@ from dcs.unitgroup import StaticGroup from game import db from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.db import unit_type_from_name -from .airfields import RunwayData from .conflictgen import Conflict from .radios import RadioRegistry +from .runways import RunwayData from .tacan import TacanBand, TacanRegistry FARP_FRONTLINE_DISTANCE = 10000 @@ -141,8 +141,9 @@ class GroundObjectsGenerator: # Find carrier direction (In the wind) found_carrier_destination = False attempt = 0 + brc = self.m.weather.wind_at_ground.direction + 180 while not found_carrier_destination and attempt < 5: - point = sg.points[0].position.point_from_heading(self.m.weather.wind_at_ground.direction + 180, 100000-attempt*20000) + point = sg.points[0].position.point_from_heading(brc, 100000-attempt*20000) if self.game.theater.is_in_sea(point): found_carrier_destination = True sg.add_waypoint(point) @@ -196,6 +197,7 @@ class GroundObjectsGenerator: # unit name since it's an arbitrary ID. self.runways[cp.name] = RunwayData( cp.name, + brc, "N/A", atc=atc_channel, tacan=tacan, diff --git a/gen/kneeboard.py b/gen/kneeboard.py index a0c4c7a5..d2782188 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -22,14 +22,13 @@ https://forums.eagle.ru/showthread.php?t=206360 claims that kneeboard pages can 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. """ +import datetime from collections import defaultdict from dataclasses import dataclass -import datetime from pathlib import Path from typing import Dict, List, Optional, Tuple from PIL import Image, ImageDraw, ImageFont -from dcs.mapping import Point from dcs.mission import Mission from dcs.unittype import FlyingType from tabulate import tabulate @@ -37,12 +36,11 @@ from tabulate import tabulate from game.utils import meter_to_nm from . import units from .aircraft import AIRCRAFT_DATA, FlightData -from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator from .flights.flight import FlightWaypoint, FlightWaypointType -from .flights.traveltime import TravelTime from .radios import RadioFrequency +from .runways import RunwayData class KneeboardPageWriter: diff --git a/gen/runways.py b/gen/runways.py new file mode 100644 index 00000000..5323c37b --- /dev/null +++ b/gen/runways.py @@ -0,0 +1,139 @@ +"""Runway information and selection.""" +from __future__ import annotations + +import logging +from dataclasses import dataclass +from typing import Iterator, Optional + +from dcs.terrain.terrain import Airport + +from game.weather import Conditions +from theater import ControlPoint, ControlPointType +from .airfields import AIRFIELD_DATA +from .radios import RadioFrequency +from .tacan import TacanChannel + + +@dataclass(frozen=True) +class RunwayData: + airfield_name: str + runway_heading: int + runway_name: str + atc: Optional[RadioFrequency] = None + tacan: Optional[TacanChannel] = None + tacan_callsign: Optional[str] = None + ils: Optional[RadioFrequency] = None + icls: Optional[int] = None + + @classmethod + def for_airfield(cls, airport: Airport, runway_heading: int, + runway_name: str) -> RunwayData: + """Creates RunwayData for the given runway of an airfield. + + Args: + airport: The airfield the runway belongs to. + runway_heading: Heading of the runway. + runway_name: Identifier of the runway to use. e.g. "03" or "20L". + """ + atc: Optional[RadioFrequency] = None + tacan: Optional[TacanChannel] = None + tacan_callsign: Optional[str] = None + ils: Optional[RadioFrequency] = None + try: + airfield = AIRFIELD_DATA[airport.name] + if airfield.atc is not None: + atc = airfield.atc.uhf + else: + atc = None + tacan = airfield.tacan + tacan_callsign = airfield.tacan_callsign + ils = airfield.ils_freq(runway_name) + except KeyError: + logging.warning(f"No airfield data for {airport.name}") + return cls( + airfield_name=airport.name, + runway_heading=runway_heading, + runway_name=runway_name, + atc=atc, + tacan=tacan, + tacan_callsign=tacan_callsign, + ils=ils + ) + + @classmethod + def for_pydcs_airport(cls, airport: Airport) -> Iterator[RunwayData]: + for runway in airport.runways: + runway_number = runway.heading // 10 + runway_side = ["", "L", "R"][runway.leftright] + runway_name = f"{runway_number:02}{runway_side}" + yield cls.for_airfield(airport, runway.heading, runway_name) + + # pydcs only exposes one runway per physical runway, so to expose + # both sides of the runway we need to generate the other. + heading = (runway.heading + 180) % 360 + runway_number = heading // 10 + runway_side = ["", "R", "L"][runway.leftright] + runway_name = f"{runway_number:02}{runway_side}" + yield cls.for_airfield(airport, heading, runway_name) + + +class RunwayAssigner: + def __init__(self, conditions: Conditions): + self.conditions = conditions + + def angle_off_headwind(self, runway: RunwayData) -> int: + wind = self.conditions.weather.wind.at_0m.direction + ideal_heading = (wind + 180) % 360 + return abs(runway.runway_heading - ideal_heading) + + def get_preferred_runway(self, airport: Airport) -> RunwayData: + """Returns the preferred runway for the given airport. + + Right now we're only selecting runways based on whether or not + they have + ILS, but we could also choose based on wind conditions, or which + direction flight plans should follow. + """ + runways = list(RunwayData.for_pydcs_airport(airport)) + + # Find the runway with the best headwind first. + best_runways = [runways[0]] + best_angle_off_headwind = self.angle_off_headwind(best_runways[0]) + for runway in runways[1:]: + angle_off_headwind = self.angle_off_headwind(runway) + if angle_off_headwind == best_angle_off_headwind: + best_runways.append(runway) + elif angle_off_headwind < best_angle_off_headwind: + best_runways = [runway] + best_angle_off_headwind = angle_off_headwind + + for runway in best_runways: + # But if there are multiple runways with the same heading, + # prefer + # and ILS capable runway. + if runway.ils is not None: + return runway + + # Otherwise the only difference between the two is the distance from + # parking, which we don't know, so just pick the first one. + return best_runways[0] + + def takeoff_heading(self, departure: ControlPoint) -> int: + if departure.cptype == ControlPointType.AIRBASE: + return self.get_preferred_runway(departure.airport).runway_heading + elif departure.is_fleet: + # The carrier will be angled into the wind automatically. + return (self.conditions.weather.wind.at_0m.direction + 180) % 360 + logging.warning( + f"Unhandled departure control point: {departure.cptype}") + return 0 + + def landing_heading(self, arrival: ControlPoint) -> int: + if arrival.cptype == ControlPointType.AIRBASE: + return self.get_preferred_runway(arrival.airport).runway_heading + elif arrival.is_fleet: + # The carrier will be angled into the wind automatically. + return (self.conditions.weather.wind.at_0m.direction + 180) % 360 + logging.warning( + f"Unhandled departure control point: {arrival.cptype}") + return 0 From c3fca6696d4f631b1c870f12251b4a47c79cc7d3 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 23 Oct 2020 14:52:14 -0700 Subject: [PATCH 04/10] Fix waypoint altitudes for helicopters. Fixes https://github.com/Khopa/dcs_liberation/issues/234 --- gen/flights/flightplan.py | 9 +++----- gen/flights/waypointbuilder.py | 38 ++++++++++++++++++---------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 7c9f26f1..2d8eda20 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -351,9 +351,6 @@ class FlightPlanBuilder: if not isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) - is_helo = getattr(flight.unit_type, "helicopter", False) - cap_alt = 500 if is_helo else 1000 - ingress, heading, distance = Conflict.frontline_vector( location.control_points[0], location.control_points[1], self.game.theater @@ -362,14 +359,14 @@ class FlightPlanBuilder: egress = ingress.point_from_heading(heading, distance) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) - builder.ascent(flight.from_cp, is_helo) + builder.ascent(flight.from_cp) builder.hold(self._hold_point(flight)) builder.join(self.package.waypoints.join) builder.ingress_cas(ingress, location) - builder.cas(center, cap_alt) + builder.cas(center) builder.egress(egress, location) builder.split(self.package.waypoints.split) - builder.rtb(flight.from_cp, is_helo) + builder.rtb(flight.from_cp) flight.points = builder.build() diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index cdaefd0b..14583c6b 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -22,15 +22,18 @@ class WaypointBuilder: self.waypoints: List[FlightWaypoint] = [] self.ingress_point: Optional[FlightWaypoint] = None + @property + def is_helo(self) -> bool: + return getattr(self.flight.unit_type, "helicopter", False) + def build(self) -> List[FlightWaypoint]: return self.waypoints - def ascent(self, departure: ControlPoint, is_helo: bool = False) -> None: + def ascent(self, departure: ControlPoint) -> None: """Create ascent waypoint for the given departure airfield or carrier. Args: departure: Departure airfield or carrier. - is_helo: True if the flight is a helicopter. """ heading = RunwayAssigner(self.conditions).takeoff_heading(departure) position = departure.position.point_from_heading( @@ -40,7 +43,7 @@ class WaypointBuilder: FlightWaypointType.ASCEND_POINT, position.x, position.y, - 500 if is_helo else self.doctrine.pattern_altitude + 500 if self.is_helo else self.doctrine.pattern_altitude ) waypoint.name = "ASCEND" waypoint.alt_type = "RADIO" @@ -48,14 +51,14 @@ class WaypointBuilder: waypoint.pretty_name = "Ascend" self.waypoints.append(waypoint) - def descent(self, arrival: ControlPoint, is_helo: bool = False) -> None: + def descent(self, arrival: ControlPoint) -> None: """Create descent waypoint for the given arrival airfield or carrier. Args: arrival: Arrival airfield or carrier. - is_helo: True if the flight is a helicopter. """ - landing_heading = RunwayAssigner(self.conditions).landing_heading(arrival) + landing_heading = RunwayAssigner(self.conditions).landing_heading( + arrival) heading = (landing_heading + 180) % 360 position = arrival.position.point_from_heading( heading, nm_to_meter(5) @@ -64,7 +67,7 @@ class WaypointBuilder: FlightWaypointType.DESCENT_POINT, position.x, position.y, - 300 if is_helo else self.doctrine.pattern_altitude + 300 if self.is_helo else self.doctrine.pattern_altitude ) waypoint.name = "DESCEND" waypoint.alt_type = "RADIO" @@ -96,7 +99,7 @@ class WaypointBuilder: FlightWaypointType.LOITER, position.x, position.y, - self.doctrine.rendezvous_altitude + 500 if self.is_helo else self.doctrine.rendezvous_altitude ) waypoint.pretty_name = "Hold" waypoint.description = "Wait until push time" @@ -108,7 +111,7 @@ class WaypointBuilder: FlightWaypointType.JOIN, position.x, position.y, - self.doctrine.ingress_altitude + 500 if self.is_helo else self.doctrine.ingress_altitude ) waypoint.pretty_name = "Join" waypoint.description = "Rendezvous with package" @@ -120,7 +123,7 @@ class WaypointBuilder: FlightWaypointType.SPLIT, position.x, position.y, - self.doctrine.ingress_altitude + 500 if self.is_helo else self.doctrine.ingress_altitude ) waypoint.pretty_name = "Split" waypoint.description = "Depart from package" @@ -148,7 +151,7 @@ class WaypointBuilder: ingress_type, position.x, position.y, - self.doctrine.ingress_altitude + 500 if self.is_helo else self.doctrine.ingress_altitude ) waypoint.pretty_name = "INGRESS on " + objective.name waypoint.description = "INGRESS on " + objective.name @@ -161,7 +164,7 @@ class WaypointBuilder: FlightWaypointType.EGRESS, position.x, position.y, - self.doctrine.ingress_altitude + 500 if self.is_helo else self.doctrine.ingress_altitude ) waypoint.pretty_name = "EGRESS from " + target.name waypoint.description = "EGRESS from " + target.name @@ -248,12 +251,12 @@ class WaypointBuilder: # TODO: This seems wrong, but it's what was there before. self.ingress_point.targets.append(location) - def cas(self, position: Point, altitude: int) -> None: + def cas(self, position: Point) -> None: waypoint = FlightWaypoint( FlightWaypointType.CAS, position.x, position.y, - altitude + 500 if self.is_helo else 1000 ) waypoint.alt_type = "RADIO" waypoint.description = "Provide CAS" @@ -308,14 +311,13 @@ class WaypointBuilder: self.race_track_start(start, altitude) self.race_track_end(end, altitude) - def rtb(self, arrival: ControlPoint, is_helo: bool = False) -> None: + def rtb(self, arrival: ControlPoint) -> None: """Creates descent ant landing waypoints for the given control point. Args: arrival: Arrival airfield or carrier. - is_helo: True if the flight is a helicopter. """ - self.descent(arrival, is_helo) + self.descent(arrival) self.land(arrival) def escort(self, ingress: Point, target: MissionTarget, @@ -339,7 +341,7 @@ class WaypointBuilder: FlightWaypointType.TARGET_GROUP_LOC, target.position.x, target.position.y, - self.doctrine.ingress_altitude + 500 if self.is_helo else self.doctrine.ingress_altitude ) waypoint.name = "TARGET" waypoint.description = "Escort the package" From 15db12fb2125dce79e504e10138213a401f7703f Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 23 Oct 2020 22:28:51 -0700 Subject: [PATCH 05/10] Fix TOT/start for BARCAPs in other packages. We were only getting BARCAP results right in BARCAP packages. This fixes calculations of TOTs and start times for BARCAPs in strike packages. The probably needs some refactoring. BARCAP is just the symptomatic example at the moment, but the real problem is that different mission profiles exist and we currently only handle one. Making profiles explicit in mission planning will clean this up, will be needed for other future mission types, and makes it easier for us to alter behavior for waypoint and timing decisions based on the aircraft or mission type. --- gen/aircraft.py | 5 +- gen/flights/traveltime.py | 83 +++++++++++-------- qt_ui/widgets/map/QLiberationMap.py | 2 +- .../flight/waypoints/QFlightWaypointList.py | 11 +-- 4 files changed, 57 insertions(+), 44 deletions(-) diff --git a/gen/aircraft.py b/gen/aircraft.py index 7e2ceb1a..1d36ad37 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1288,8 +1288,9 @@ class RaceTrackBuilder(PydcsWaypointBuilder): pattern=OrbitAction.OrbitPattern.RaceTrack )) - self.set_waypoint_tot(waypoint, self.timing.race_track_start) - racetrack.stop_after_time(self.timing.race_track_end) + self.set_waypoint_tot(waypoint, + self.timing.race_track_start(self.flight)) + racetrack.stop_after_time(self.timing.race_track_end(self.flight)) waypoint.add_task(racetrack) return waypoint diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py index 8fb8b7af..00c35bd0 100644 --- a/gen/flights/traveltime.py +++ b/gen/flights/traveltime.py @@ -27,21 +27,22 @@ INGRESS_TYPES = { FlightWaypointType.INGRESS_STRIKE, } -IP_TYPES = { - FlightWaypointType.INGRESS_CAS, - FlightWaypointType.INGRESS_ESCORT, - FlightWaypointType.INGRESS_SEAD, - FlightWaypointType.INGRESS_STRIKE, - FlightWaypointType.PATROL_TRACK, -} - class GroundSpeed: @staticmethod def mission_speed(package: Package) -> int: speeds = set() for flight in package.flights: - waypoint = flight.waypoint_with_type(IP_TYPES) + # Find a waypoint that matches the mission start waypoint and use + # that for the altitude of the mission. That may not be true for the + # whole mission, but it's probably good enough for now. + waypoint = flight.waypoint_with_type({ + FlightWaypointType.INGRESS_CAS, + FlightWaypointType.INGRESS_ESCORT, + FlightWaypointType.INGRESS_SEAD, + FlightWaypointType.INGRESS_STRIKE, + FlightWaypointType.PATROL_TRACK, + }) if waypoint is None: logging.error(f"Could not find ingress point for {flight}.") if flight.points: @@ -152,8 +153,10 @@ class TotEstimator: # Takeoff immediately. return 0 - if self.package.primary_task == FlightType.BARCAP: - start_time = self.timing.race_track_start + # BARCAP flights do not coordinate with the rest of the package on join + # or ingress points. + if flight.flight_type == FlightType.BARCAP: + start_time = self.timing.race_track_start(flight) else: start_time = self.timing.join return start_time - travel_time - self.HOLD_TIME @@ -166,7 +169,9 @@ class TotEstimator: def earliest_tot_for_flight(self, flight: Flight) -> int: """Estimate fastest time from mission start to the target position. - For CAP missions, this is time to race track start. + For BARCAP flights, this is time to race track start. This ensures that + they are on station at the same time any other package members reach + their ingress point. For other mission types this is the time to the mission target. @@ -177,27 +182,28 @@ class TotEstimator: The earliest possible TOT for the given flight in seconds. Returns 0 if an ingress point cannot be found. """ - time_to_ingress = self.estimate_waypoints_to_target(flight, IP_TYPES) - if time_to_ingress is None: - logging.warning( - f"Found no ingress types. Cannot estimate TOT for {flight}") - # Return 0 so this flight's travel time does not affect the rest of - # the package. - return 0 - - if self.package.primary_task == FlightType.BARCAP: - # The racetrack start *is* the target. The package target is the - # protected objective. - time_to_target = 0 + if flight.flight_type == FlightType.BARCAP: + time_to_target = self.estimate_waypoints_to_target(flight, { + FlightWaypointType.PATROL_TRACK + }) else: + time_to_ingress = self.estimate_waypoints_to_target( + flight, INGRESS_TYPES + ) + if time_to_ingress is None: + logging.warning( + f"Found no ingress types. Cannot estimate TOT for {flight}") + # Return 0 so this flight's travel time does not affect the rest + # of the package. + return 0 + assert self.package.waypoints is not None - time_to_target = TravelTime.between_points( + time_to_target = time_to_ingress + TravelTime.between_points( self.package.waypoints.ingress, self.package.target.position, GroundSpeed.mission_speed(self.package)) return sum([ self.estimate_startup(flight), self.estimate_ground_ops(flight), - time_to_ingress, time_to_target, ]) @@ -281,18 +287,22 @@ class PackageWaypointTiming: assert self.package.time_over_target is not None return self.package.time_over_target - @property - def race_track_start(self) -> int: - if self.package.primary_task == FlightType.BARCAP: - return self.package.time_over_target + def race_track_start(self, flight: Flight) -> int: + if flight.flight_type == FlightType.BARCAP: + return self.target else: + # The only other type that (currently) uses race tracks is TARCAP, + # which is sort of in need of cleanup. TARCAP is only valid on front + # lines and they participate in join points and patrol between the + # ingress and egress points rather than on a race track actually + # pointed at the enemy. return self.ingress - @property - def race_track_end(self) -> int: - if self.package.primary_task == FlightType.BARCAP: + def race_track_end(self, flight: Flight) -> int: + if flight.flight_type == FlightType.BARCAP: return self.target + CAP_DURATION * 60 else: + # For TARCAP. See the explanation in race_track_start. return self.egress def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int: @@ -303,7 +313,8 @@ class PackageWaypointTiming: GroundSpeed.for_flight(flight, hold_point.alt) ) - def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]: + def tot_for_waypoint(self, flight: Flight, + waypoint: FlightWaypoint) -> Optional[int]: target_types = ( FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_POINT, @@ -321,7 +332,7 @@ class PackageWaypointTiming: elif waypoint.waypoint_type == FlightWaypointType.SPLIT: return self.split elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK: - return self.race_track_start + return self.race_track_start(flight) return None def depart_time_for_waypoint(self, waypoint: FlightWaypoint, @@ -329,7 +340,7 @@ class PackageWaypointTiming: if waypoint.waypoint_type == FlightWaypointType.LOITER: return self.push_time(flight, waypoint) elif waypoint.waypoint_type == FlightWaypointType.PATROL: - return self.race_track_end + return self.race_track_end(flight) return None @classmethod diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 567bce80..793cd641 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -344,7 +344,7 @@ class QLiberationMap(QGraphicsView): altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL" prefix = "TOT" - time = timing.tot_for_waypoint(waypoint) + time = timing.tot_for_waypoint(flight, waypoint) if time is None: prefix = "Depart" time = timing.depart_time_for_waypoint(waypoint, flight) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py index aa904e2d..99dcc410 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py @@ -55,11 +55,12 @@ class QFlightWaypointList(QTableView): waypoints = itertools.chain([takeoff], self.flight.points) for row, waypoint in enumerate(waypoints): - self.add_waypoint_row(row, waypoint, timing) + self.add_waypoint_row(row, self.flight, waypoint, timing) self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select) - def add_waypoint_row(self, row: int, waypoint: FlightWaypoint, + def add_waypoint_row(self, row: int, flight: Flight, + waypoint: FlightWaypoint, timing: PackageWaypointTiming) -> None: self.model.insertRow(self.model.rowCount()) @@ -71,15 +72,15 @@ class QFlightWaypointList(QTableView): altitude_item.setEditable(False) self.model.setItem(row, 1, altitude_item) - tot = self.tot_text(waypoint, timing) + tot = self.tot_text(flight, waypoint, timing) tot_item = QStandardItem(tot) tot_item.setEditable(False) self.model.setItem(row, 2, tot_item) - def tot_text(self, waypoint: FlightWaypoint, + def tot_text(self, flight: Flight, waypoint: FlightWaypoint, timing: PackageWaypointTiming) -> str: prefix = "" - time = timing.tot_for_waypoint(waypoint) + time = timing.tot_for_waypoint(flight, waypoint) if time is None: prefix = "Depart " time = timing.depart_time_for_waypoint(waypoint, self.flight) From 5f02febb6c0603148ee72ec00d33e31ff60e8409 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 23 Oct 2020 22:41:28 -0700 Subject: [PATCH 06/10] Fix kneeboard crash for small strike missions. When we don't coalesce target points on the kneeboard we call add_waypoint_row multiple times, so calls after the first were using the travel time from one strike target to the next. Since they were so close that rounded down to zero and caused a divide by zero error. Update the last waypoint in add_waypoint instead. --- gen/kneeboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gen/kneeboard.py b/gen/kneeboard.py index d2782188..9cf9b258 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -124,6 +124,7 @@ class FlightPlanBuilder: self.target_points = [] self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint)) + self.last_waypoint = waypoint def coalesce_target_points(self) -> None: if len(self.target_points) <= 4: @@ -155,7 +156,6 @@ class FlightPlanBuilder: self._format_time(waypoint.waypoint.tot), self._format_time(waypoint.waypoint.departure_time), ]) - self.last_waypoint = waypoint.waypoint def _format_time(self, time: Optional[int]) -> str: if time is None: From e9bfd58ee1216729602d3b68644e4d519942197e Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 23 Oct 2020 22:52:32 -0700 Subject: [PATCH 07/10] Shorten strike waypoint descriptions. We don't really need this much detail in these, and it was overflowing the kneeboard. --- gen/flights/flightplan.py | 6 +----- gen/flights/waypointbuilder.py | 9 +++------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 2d8eda20..e7bbb1b0 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -157,11 +157,7 @@ class FlightPlanBuilder: if building.is_dead: continue - builder.strike_point( - building, - f"{building.obj_name} {building.category}", - location - ) + builder.strike_point(building, building.category, location) builder.egress(self.package.waypoints.egress, location) builder.split(self.package.waypoints.split) diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 14583c6b..46801280 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -173,24 +173,21 @@ class WaypointBuilder: def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str, location: MissionTarget) -> None: - self._target_point(target, name, f"STRIKE [{location.name}]: {name}", - location) + self._target_point(target, name, f"STRIKE {name}", location) # TODO: Seems fishy. if self.ingress_point is not None: self.ingress_point.targetGroup = location def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str, location: MissionTarget) -> None: - self._target_point(target, name, f"STRIKE [{location.name}]: {name}", - location) + self._target_point(target, name, f"STRIKE {name}", location) # TODO: Seems fishy. if self.ingress_point is not None: self.ingress_point.targetGroup = location def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str, location: MissionTarget) -> None: - self._target_point(target, name, f"STRIKE [{location.name}]: {name}", - location) + self._target_point(target, name, f"STRIKE {name}", location) def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str, description: str, location: MissionTarget) -> None: From c06a85511351639929018776286f415028534baa Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 24 Oct 2020 00:40:02 -0700 Subject: [PATCH 08/10] Fix mypy regressions. Mostly in the plugin system, which needed a handful of asserts that shouldn't be necessary, but fixing them requires a refactor. --- game/operation/operation.py | 55 +++++++------- gen/aircraft.py | 76 +++++++++++-------- gen/flights/ai_flight_planner.py | 7 +- gen/flights/flight.py | 14 +++- gen/flights/traveltime.py | 6 ++ plugin/luaplugin.py | 63 ++++++++------- .../windows/mission/flight/QFlightCreator.py | 2 +- theater/landmap.py | 2 +- 8 files changed, 129 insertions(+), 96 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 98feb740..3c32210c 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -14,7 +14,7 @@ from dcs.translation import String from dcs.triggers import TriggerStart from dcs.unittype import UnitType -from gen import Conflict, VisualGenerator, FlightType +from gen import Conflict, FlightType, VisualGenerator from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData from gen.airfields import AIRFIELD_DATA from gen.airsupportgen import AirSupport, AirSupportConflictGenerator @@ -28,10 +28,11 @@ from gen.kneeboard import KneeboardGenerator from gen.radios import RadioFrequency, RadioRegistry from gen.tacan import TacanRegistry from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator +from plugin import LuaPluginManager from theater import ControlPoint from .. import db from ..debriefing import Debriefing -from plugin import LuaPluginManager + class Operation: attackers_starting_position = None # type: db.StartingPosition @@ -74,7 +75,7 @@ class Operation: self.departure_cp = departure_cp self.to_cp = to_cp self.is_quick = False - self.listOfPluginsScripts = [] + self.plugin_scripts: List[str] = [] def units_of(self, country_name: str) -> List[UnitType]: return [] @@ -133,33 +134,37 @@ class Operation: else: self.defenders_starting_position = None - def injectLuaTrigger(self, luascript, comment = "LUA script"): + def inject_lua_trigger(self, contents: str, comment: str) -> None: trigger = TriggerStart(comment=comment) - trigger.add_action(DoScript(String(luascript))) + trigger.add_action(DoScript(String(contents))) self.current_mission.triggerrules.triggers.append(trigger) - def bypassPluginScript(self, pluginName, scriptFileMnemonic): - self.listOfPluginsScripts.append(scriptFileMnemonic) + def bypass_plugin_script(self, mnemonic: str) -> None: + self.plugin_scripts.append(mnemonic) - def injectPluginScript(self, pluginName, scriptFile, scriptFileMnemonic): - if not scriptFileMnemonic in self.listOfPluginsScripts: - self.listOfPluginsScripts.append(scriptFileMnemonic) + def inject_plugin_script(self, plugin_mnemonic: str, script: str, + script_mnemonic: str) -> None: + if script_mnemonic in self.plugin_scripts: + logging.debug( + f"Skipping already loaded {script} for {plugin_mnemonic}" + ) - plugin_path = Path("./resources/plugins",pluginName) + self.plugin_scripts.append(script_mnemonic) - if scriptFile != None: - scriptFile_path = Path(plugin_path, scriptFile) - if scriptFile_path.exists(): - trigger = TriggerStart(comment="Load " + scriptFileMnemonic) - filename = scriptFile_path.resolve() - fileref = self.current_mission.map_resource.add_resource_file(filename) - trigger.add_action(DoScriptFile(fileref)) - self.current_mission.triggerrules.triggers.append(trigger) - else: - logging.error(f"Cannot find script file {scriptFile} for plugin {pluginName}") + plugin_path = Path("./resources/plugins", plugin_mnemonic) - else: - logging.debug(f"Skipping script file {scriptFile} for plugin {pluginName}") + script_path = Path(plugin_path, script) + if not script_path.exists(): + logging.error( + f"Cannot find {script_path} for plugin {plugin_mnemonic}" + ) + return + + trigger = TriggerStart(comment=f"Load {script_mnemonic}") + filename = script_path.resolve() + fileref = self.current_mission.map_resource.add_resource_file(filename) + trigger.add_action(DoScriptFile(fileref)) + self.current_mission.triggerrules.triggers.append(trigger) def generate(self): radio_registry = RadioRegistry() @@ -334,7 +339,7 @@ class Operation: kneeboard_generator.add_flight(flight) if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]: flightType = flight.flight_type.name - flightTarget = flight.targetPoint + flightTarget = flight.package.target if flightTarget: flightTargetName = None flightTargetType = None @@ -453,8 +458,6 @@ dcsLiberation.TargetPoints = { self.current_mission.triggerrules.triggers.append(trigger) # Inject Plugins Lua Scripts and data - self.listOfPluginsScripts = [] - for plugin in LuaPluginManager().getPlugins(): plugin.injectScripts(self) plugin.injectConfiguration(self) diff --git a/gen/aircraft.py b/gen/aircraft.py index 1d36ad37..e51964fa 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -205,6 +205,9 @@ class ChannelAssignment: class FlightData: """Details of a planned flight.""" + #: The package that the flight belongs to. + package: Package + flight_type: FlightType #: All units in the flight. @@ -237,14 +240,13 @@ class FlightData: #: Map of radio frequencies to their assigned radio and channel, if any. frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] - #: Data concerning the target of a CAS/Strike/SEAD flight, or None else - targetPoint = None - - def __init__(self, flight_type: FlightType, units: List[FlyingUnit], - size: int, friendly: bool, departure_delay: int, - departure: RunwayData, arrival: RunwayData, - divert: Optional[RunwayData], waypoints: List[FlightWaypoint], - intra_flight_channel: RadioFrequency, targetPoint: Optional) -> None: + def __init__(self, package: Package, flight_type: FlightType, + units: List[FlyingUnit], size: int, friendly: bool, + departure_delay: int, departure: RunwayData, + arrival: RunwayData, divert: Optional[RunwayData], + waypoints: List[FlightWaypoint], + intra_flight_channel: RadioFrequency) -> None: + self.package = package self.flight_type = flight_type self.units = units self.size = size @@ -257,7 +259,6 @@ class FlightData: self.intra_flight_channel = intra_flight_channel self.frequency_to_channel_map = {} self.callsign = create_group_callsign_from_unit(self.units[0]) - self.targetPoint = targetPoint @property def client_units(self) -> List[FlyingUnit]: @@ -575,7 +576,8 @@ class AircraftConflictGenerator: return StartType.Warm def _setup_group(self, group: FlyingGroup, for_task: Type[Task], - flight: Flight, dynamic_runways: Dict[str, RunwayData]): + package: Package, flight: Flight, + dynamic_runways: Dict[str, RunwayData]) -> None: did_load_loadout = False unit_type = group.units[0].unit_type @@ -635,6 +637,7 @@ class AircraftConflictGenerator: departure_runway = fallback_runway self.flights.append(FlightData( + package=package, flight_type=flight.flight_type, units=group.units, size=len(group.units), @@ -646,8 +649,7 @@ class AircraftConflictGenerator: divert=None, # Waypoints are added later, after they've had their TOTs set. waypoints=[], - intra_flight_channel=channel, - targetPoint=flight.targetPoint, + intra_flight_channel=channel )) # Special case so Su 33 carrier take off @@ -789,7 +791,7 @@ class AircraftConflictGenerator: logging.info(f"Generating flight: {flight.unit_type}") group = self.generate_planned_flight(flight.from_cp, country, flight) - self.setup_flight_group(group, flight, dynamic_runways) + self.setup_flight_group(group, package, flight, dynamic_runways) self.create_waypoints(group, package, flight, timing) def set_activation_time(self, flight: Flight, group: FlyingGroup, @@ -906,10 +908,11 @@ class AircraftConflictGenerator: if flight.unit_type.eplrs: group.points[0].tasks.append(EPLRS(group.id)) - def configure_cap(self, group: FlyingGroup, flight: Flight, + def configure_cap(self, group: FlyingGroup, package: Package, + flight: Flight, dynamic_runways: Dict[str, RunwayData]) -> None: group.task = CAP.name - self._setup_group(group, CAP, flight, dynamic_runways) + self._setup_group(group, CAP, package, flight, dynamic_runways) if flight.unit_type not in GUNFIGHTERS: ammo_type = OptRTBOnOutOfAmmo.Values.AAM @@ -921,10 +924,11 @@ class AircraftConflictGenerator: group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), targets=[Targets.All.Air])) - def configure_cas(self, group: FlyingGroup, flight: Flight, + def configure_cas(self, group: FlyingGroup, package: Package, + flight: Flight, dynamic_runways: Dict[str, RunwayData]) -> None: group.task = CAS.name - self._setup_group(group, CAS, flight, dynamic_runways) + self._setup_group(group, CAS, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -936,10 +940,11 @@ class AircraftConflictGenerator: targets=[Targets.All.GroundUnits.GroundVehicles]) ) - def configure_sead(self, group: FlyingGroup, flight: Flight, - dynamic_runways: Dict[str, RunwayData]) -> None: + def configure_sead(self, group: FlyingGroup, package: Package, + flight: Flight, + dynamic_runways: Dict[str, RunwayData]) -> None: group.task = SEAD.name - self._setup_group(group, SEAD, flight, dynamic_runways) + self._setup_group(group, SEAD, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -947,33 +952,37 @@ class AircraftConflictGenerator: rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM, restrict_jettison=True) - def configure_strike(self, group: FlyingGroup, flight: Flight, + def configure_strike(self, group: FlyingGroup, package: Package, + flight: Flight, dynamic_runways: Dict[str, RunwayData]) -> None: group.task = PinpointStrike.name - self._setup_group(group, GroundAttack, flight, dynamic_runways) + self._setup_group(group, GroundAttack, package, flight, dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, roe=OptROE.Values.OpenFire, restrict_jettison=True) - def configure_anti_ship(self, group: FlyingGroup, flight: Flight, + def configure_anti_ship(self, group: FlyingGroup, package: Package, + flight: Flight, dynamic_runways: Dict[str, RunwayData]) -> None: group.task = AntishipStrike.name - self._setup_group(group, AntishipStrike, flight, dynamic_runways) + self._setup_group(group, AntishipStrike, package, flight, + dynamic_runways) self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, roe=OptROE.Values.OpenFire, restrict_jettison=True) - def configure_escort(self, group: FlyingGroup, flight: Flight, + def configure_escort(self, group: FlyingGroup, package: Package, + flight: Flight, dynamic_runways: Dict[str, RunwayData]) -> None: # Escort groups are actually given the CAP task so they can perform the # Search Then Engage task, which we have to use instead of the Escort # task for the reasons explained in JoinPointBuilder. group.task = CAP.name - self._setup_group(group, CAP, flight, dynamic_runways) + self._setup_group(group, CAP, package, flight, dynamic_runways) self.configure_behavior(group, roe=OptROE.Values.OpenFire, restrict_jettison=True) @@ -982,22 +991,23 @@ class AircraftConflictGenerator: logging.error(f"Unhandled flight type: {flight.flight_type.name}") self.configure_behavior(group) - def setup_flight_group(self, group: FlyingGroup, flight: Flight, + def setup_flight_group(self, group: FlyingGroup, package: Package, + flight: Flight, dynamic_runways: Dict[str, RunwayData]) -> None: flight_type = flight.flight_type if flight_type in [FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION]: - self.configure_cap(group, flight, dynamic_runways) + self.configure_cap(group, package, flight, dynamic_runways) elif flight_type in [FlightType.CAS, FlightType.BAI]: - self.configure_cas(group, flight, dynamic_runways) + self.configure_cas(group, package, flight, dynamic_runways) elif flight_type in [FlightType.SEAD, FlightType.DEAD]: - self.configure_sead(group, flight, dynamic_runways) + self.configure_sead(group, package, flight, dynamic_runways) elif flight_type in [FlightType.STRIKE]: - self.configure_strike(group, flight, dynamic_runways) + self.configure_strike(group, package, flight, dynamic_runways) elif flight_type in [FlightType.ANTISHIP]: - self.configure_anti_ship(group, flight, dynamic_runways) + self.configure_anti_ship(group, package, flight, dynamic_runways) elif flight_type == FlightType.ESCORT: - self.configure_escort(group, flight, dynamic_runways) + self.configure_escort(group, package, flight, dynamic_runways) else: self.configure_unknown_task(group, flight) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 0ea53eb6..c3157eee 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -206,10 +206,9 @@ class PackageBuilder: if assignment is None: return False airfield, aircraft = assignment - flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task, - self.start_type) + flight = Flight(self.package, aircraft, plan.num_aircraft, airfield, + plan.task, self.start_type) self.package.add_flight(flight) - flight.targetPoint = self.package.target return True def build(self) -> Package: @@ -222,7 +221,7 @@ class PackageBuilder: for flight in flights: self.global_inventory.return_from_flight(flight) self.package.remove_flight(flight) - flight.targetPoint = None + class ObjectiveFinder: """Identifies potential objectives for the mission planner.""" diff --git a/gen/flights/flight.py b/gen/flights/flight.py index f47a8489..2fd9a7fe 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from enum import Enum -from typing import Dict, Iterable, List, Optional +from typing import Dict, Iterable, List, Optional, TYPE_CHECKING from dcs.mapping import Point from dcs.point import MovingPoint, PointAction @@ -8,6 +10,9 @@ from dcs.unittype import UnitType from game import db from theater.controlpoint import ControlPoint, MissionTarget +if TYPE_CHECKING: + from gen.ato import Package + class FlightType(Enum): CAP = 0 # Do not use. Use BARCAP or TARCAP. @@ -138,10 +143,11 @@ class Flight: use_custom_loadout = False preset_loadout_name = "" group = False # Contains DCS Mission group data after mission has been generated - targetPoint = None # Contains either None or a Strike/SEAD target point location - def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint, - flight_type: FlightType, start_type: str) -> None: + def __init__(self, package: Package, unit_type: UnitType, count: int, + from_cp: ControlPoint, flight_type: FlightType, + start_type: str) -> None: + self.package = package self.unit_type = unit_type self.count = count self.from_cp = from_cp diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py index 00c35bd0..96b04fdf 100644 --- a/gen/flights/traveltime.py +++ b/gen/flights/traveltime.py @@ -186,6 +186,12 @@ class TotEstimator: time_to_target = self.estimate_waypoints_to_target(flight, { FlightWaypointType.PATROL_TRACK }) + if time_to_target is None: + logging.warning( + f"Found no race track. Cannot estimate TOT for {flight}") + # Return 0 so this flight's travel time does not affect the rest + # of the package. + return 0 else: time_to_ingress = self.estimate_waypoints_to_target( flight, INGRESS_TYPES diff --git a/plugin/luaplugin.py b/plugin/luaplugin.py index 7bc4f57a..25d53dc4 100644 --- a/plugin/luaplugin.py +++ b/plugin/luaplugin.py @@ -1,44 +1,48 @@ -from typing import List -from pathlib import Path -from PySide2.QtCore import QSize, Qt, QItemSelectionModel, QPoint -from PySide2.QtWidgets import QLabel, QDialog, QGridLayout, QListView, QStackedLayout, QComboBox, QWidget, \ - QAbstractItemView, QPushButton, QGroupBox, QCheckBox, QVBoxLayout, QSpinBox import json +from pathlib import Path +from typing import List, Optional -class LuaPluginWorkOrder(): - - def __init__(self, parent, filename:str, mnemonic:str, disable:bool): +from PySide2.QtCore import Qt +from PySide2.QtWidgets import QCheckBox, QGridLayout, QGroupBox, QLabel + + +class LuaPluginWorkOrder: + + def __init__(self, parent, filename: str, mnemonic: str, + disable: bool) -> None: self.filename = filename self.mnemonic = mnemonic self.disable = disable self.parent = parent - def work(self, mnemonic:str, operation): + def work(self, operation): if self.disable: - operation.bypassPluginScript(self.parent.mnemonic, self.mnemonic) + operation.bypass_plugin_script(self.mnemonic) else: - operation.injectPluginScript(self.parent.mnemonic, self.filename, self.mnemonic) + operation.inject_plugin_script(self.parent.mnemonic, self.filename, + self.mnemonic) -class LuaPluginSpecificOption(): - - def __init__(self, parent, mnemonic:str, nameInUI:str, defaultValue:bool): +class LuaPluginSpecificOption: + + def __init__(self, parent, mnemonic: str, nameInUI: str, + defaultValue: bool) -> None: self.mnemonic = mnemonic self.nameInUI = nameInUI self.defaultValue = defaultValue self.parent = parent -class LuaPlugin(): +class LuaPlugin: NAME_IN_SETTINGS_BASE:str = "plugins." - def __init__(self, jsonFilename:str): - self.mnemonic:str = None - self.skipUI:bool = False - self.nameInUI:str = None - self.nameInSettings:str = None - self.defaultValue:bool = False - self.specificOptions = [] - self.scriptsWorkOrders: List[LuaPluginWorkOrder] = None - self.configurationWorkOrders: List[LuaPluginWorkOrder] = None + def __init__(self, jsonFilename: str) -> None: + self.mnemonic: Optional[str] = None + self.skipUI: bool = False + self.nameInUI: Optional[str] = None + self.nameInSettings: Optional[str] = None + self.defaultValue: bool = False + self.specificOptions: List[LuaPluginSpecificOption] = [] + self.scriptsWorkOrders: List[LuaPluginWorkOrder] = [] + self.configurationWorkOrders: List[LuaPluginWorkOrder] = [] self.initFromJson(jsonFilename) self.enabled = self.defaultValue self.settings = None @@ -50,6 +54,7 @@ class LuaPlugin(): self.mnemonic = jsonData.get("mnemonic") self.skipUI = jsonData.get("skipUI", False) self.nameInUI = jsonData.get("nameInUI") + assert self.mnemonic is not None self.nameInSettings = LuaPlugin.NAME_IN_SETTINGS_BASE + self.mnemonic self.defaultValue = jsonData.get("defaultValue", False) self.specificOptions = [] @@ -76,6 +81,9 @@ class LuaPlugin(): self.setSettings(settingsWindow.game.settings) if not self.skipUI: + assert self.nameInSettings is not None + assert self.settings is not None + # create the plugin choice checkbox interface self.uiWidget: QCheckBox = QCheckBox() self.uiWidget.setChecked(self.isEnabled()) @@ -95,6 +103,7 @@ class LuaPlugin(): # browse each option in the specific options list row = 0 for specificOption in self.specificOptions: + assert specificOption.mnemonic is not None nameInSettings = self.nameInSettings + "." + specificOption.mnemonic if not nameInSettings in self.settings.plugins: self.settings.plugins[nameInSettings] = specificOption.defaultValue @@ -149,7 +158,7 @@ class LuaPlugin(): # execute the work order if self.scriptsWorkOrders != None: for workOrder in self.scriptsWorkOrders: - workOrder.work(self.mnemonic, operation) + workOrder.work(operation) # serves for subclasses return self.isEnabled() @@ -177,12 +186,12 @@ class LuaPlugin(): lua += defineAllOptions lua += "end" - operation.injectLuaTrigger(lua, f"{self.mnemonic} plugin configuration") + operation.inject_lua_trigger(lua, f"{self.mnemonic} plugin configuration") # execute the work order if self.configurationWorkOrders != None: for workOrder in self.configurationWorkOrders: - workOrder.work(self.mnemonic, operation) + workOrder.work(operation) # serves for subclasses return self.isEnabled() diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index a2ca14ee..3fa8a8f8 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -107,7 +107,7 @@ class QFlightCreator(QDialog): start_type = "Cold" else: start_type = "Warm" - flight = Flight(aircraft, size, origin, task, start_type) + flight = Flight(self.package, aircraft, size, origin, task, start_type) flight.scheduled_in = self.package.delay flight.client_count = self.client_slots_spinner.value() diff --git a/theater/landmap.py b/theater/landmap.py index 6eaaf5fe..b1503e38 100644 --- a/theater/landmap.py +++ b/theater/landmap.py @@ -2,7 +2,7 @@ import pickle from typing import Collection, Optional, Tuple Zone = Collection[Tuple[float, float]] -Landmap = Tuple[Collection[Zone], Collection[Zone]] +Landmap = Tuple[Collection[Zone], Collection[Zone], Collection[Zone]] def load_landmap(filename: str) -> Optional[Landmap]: From 0f1d2b8685c5573c7f369962fcaea3211cb016c5 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 24 Oct 2020 01:10:15 -0700 Subject: [PATCH 09/10] Remove a plugin we don't include. --- resources/plugins/plugins.json | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/plugins/plugins.json b/resources/plugins/plugins.json index 21b44606..5b809d07 100644 --- a/resources/plugins/plugins.json +++ b/resources/plugins/plugins.json @@ -1,5 +1,4 @@ [ - "veaf", "jtacautolase", "base" ] From 04d3ba4c473edf53f247bbfb926f135518822cc9 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 24 Oct 2020 01:18:29 -0700 Subject: [PATCH 10/10] Add mypy to github actions so we stop regressing. --- .github/workflows/build.yml | 3 +++ .github/workflows/release.yml | 3 +++ requirements.txt | 6 ++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d0da6a6..48e79b89 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,9 @@ jobs: - name: Build binaries run: | ./venv/scripts/activate + mypy game + mypy gen + mypy theater $env:PYTHONPATH=".;./pydcs" pyinstaller pyinstaller.spec diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5fad7bff..33d78ae3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,9 @@ jobs: - name: Build binaries run: | ./venv/scripts/activate + mypy game + mypy gen + mypy theater $env:PYTHONPATH=".;./pydcs" pyinstaller pyinstaller.spec diff --git a/requirements.txt b/requirements.txt index 12d48655..3ce6e8d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ -#pydcs>=0.9.10 Pyside2>=5.13.0 pyinstaller==3.6 pyproj==2.6.1.post1 Pillow~=7.2.0 -tabulate~=0.8.7 \ No newline at end of file +tabulate~=0.8.7 + +mypy==0.782 +mypy-extensions==0.4.3 \ No newline at end of file