diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 70d466be..6c5955b8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,7 +28,8 @@ We will usually need more information for debugging. Include as much of the foll - DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`). - The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`). -- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are locaed in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`). +- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are located in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`). +- The state.json file from the finished mission when the problem is related to results processing. By default these are located in your Liberation install directory. **Version information (please complete the following information):** - DCS Liberation [e.g. 2.3.1]: diff --git a/changelog.md b/changelog.md index b9c75221..c4a7a8aa 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,9 @@ Saves from 2.4 are not compatible with 2.5. ## Fixes +* **[Flight Planner]** Front lines now project threat zones, so TARCAP/escorts will not be pruned for flights near the front. Packages may also route around the front line when practical. +* **[Flight Planner]** Fixed error when planning BAI at SAMs with dead subgroups. + # 2.4.3 ## Features/Improvements diff --git a/game/db.py b/game/db.py index 82a80fed..49202765 100644 --- a/game/db.py +++ b/game/db.py @@ -180,6 +180,7 @@ from pydcs_extensions.su57.su57 import Su_57 UNITINFOTEXT_PATH = Path("./resources/units/unit_info_text.json") plane_map["A-4E-C"] = A_4E_C +plane_map["F-22A"] = F_22A plane_map["MB-339PAN"] = MB_339PAN plane_map["Rafale_M"] = Rafale_M plane_map["Rafale_A_S"] = Rafale_A_S diff --git a/game/navmesh.py b/game/navmesh.py index bcef8191..193dd31e 100644 --- a/game/navmesh.py +++ b/game/navmesh.py @@ -21,6 +21,10 @@ from game.threatzones import ThreatZones from game.utils import nautical_miles +class NavMeshError(RuntimeError): + pass + + class NavMeshPoly: def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None: self.ident = ident @@ -125,7 +129,7 @@ class NavMesh: path.append(current.world_point) previous = came_from[current] if previous is None: - raise RuntimeError( + raise NavMeshError( f"Could not reconstruct path to {destination} from {origin}" ) current = previous @@ -140,10 +144,12 @@ class NavMesh: def shortest_path(self, origin: Point, destination: Point) -> List[Point]: origin_poly = self.localize(origin) if origin_poly is None: - raise ValueError(f"Origin point {origin} is outside the navmesh") + raise NavMeshError(f"Origin point {origin} is outside the navmesh") destination_poly = self.localize(destination) if destination_poly is None: - raise ValueError(f"Origin point {destination} is outside the navmesh") + raise NavMeshError( + f"Destination point {destination} is outside the navmesh" + ) return self._shortest_path( NavPoint(self.dcs_to_shapely_point(origin), origin_poly), @@ -203,7 +209,7 @@ class NavMesh: # threatened airbases at the map edges have room to retreat from the # threat without running off the navmesh. return box(*LineString(points).bounds).buffer( - nautical_miles(100).meters, resolution=1 + nautical_miles(200).meters, resolution=1 ) @staticmethod diff --git a/game/operation/operation.py b/game/operation/operation.py index fc190510..59822312 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -166,6 +166,7 @@ class Operation: airgen: AircraftConflictGenerator, ): """Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)""" + gens: List[MissionInfoGenerator] = [ KneeboardGenerator(cls.current_mission, cls.game), BriefingGenerator(cls.current_mission, cls.game), @@ -177,9 +178,8 @@ class Operation: for tanker in airsupportgen.air_support.tankers: gen.add_tanker(tanker) - if cls.player_awacs_enabled: - for awacs in airsupportgen.air_support.awacs: - gen.add_awacs(awacs) + for aewc in airsupportgen.air_support.awacs: + gen.add_awacs(aewc) for jtac in jtacs: gen.add_jtac(jtac) @@ -378,7 +378,9 @@ class Operation: cls.game, cls.radio_registry, cls.unit_map, + air_support=cls.airsupportgen.air_support, ) + cls.airgen.clear_parking_slots() cls.airgen.generate_flights( diff --git a/game/settings.py b/game/settings.py index 4602bd55..0e6f968a 100644 --- a/game/settings.py +++ b/game/settings.py @@ -31,6 +31,7 @@ class Settings: automate_aircraft_reinforcements: bool = False restrict_weapons_by_date: bool = False disable_legacy_aewc: bool = False + generate_dark_kneeboard: bool = False # Performance oriented perf_red_alert_state: bool = True diff --git a/game/threatzones.py b/game/threatzones.py index 865acf2f..85169f17 100644 --- a/game/threatzones.py +++ b/game/threatzones.py @@ -15,6 +15,7 @@ from shapely.ops import nearest_points, unary_union from game.theater import ControlPoint from game.utils import Distance, meters, nautical_miles +from gen import Conflict from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.flight import Flight @@ -131,7 +132,7 @@ class ThreatZones: zone belongs to the player, it is the zone that will be avoided by the enemy and vice versa. """ - airbases = [] + air_threats = [] air_defenses = [] for control_point in game.theater.controlpoints: if control_point.captured != player: @@ -139,7 +140,7 @@ class ThreatZones: if control_point.runway_is_operational(): point = ShapelyPoint(control_point.position.x, control_point.position.y) cap_threat_range = cls.barcap_threat_range(game, control_point) - airbases.append(point.buffer(cap_threat_range.meters)) + air_threats.append(point.buffer(cap_threat_range.meters)) for tgo in control_point.ground_objects: for group in tgo.groups: @@ -151,8 +152,25 @@ class ThreatZones: threat_zone = point.buffer(threat_range.meters) air_defenses.append(threat_zone) + for front_line in game.theater.conflicts(player): + vector = Conflict.frontline_vector( + front_line.control_point_a, front_line.control_point_b, game.theater + ) + + start = vector[0] + end = vector[0].point_from_heading(vector[1], vector[2]) + + line = LineString( + [ + ShapelyPoint(start.x, start.y), + ShapelyPoint(end.x, end.y), + ] + ) + doctrine = game.faction_for(player).doctrine + air_threats.append(line.buffer(doctrine.cap_engagement_range.meters)) + return cls( - airbases=unary_union(airbases), air_defenses=unary_union(air_defenses) + airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses) ) @staticmethod diff --git a/gen/aircraft.py b/gen/aircraft.py index 7363668e..024a49c4 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging import random -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import timedelta from functools import cached_property from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union @@ -67,6 +67,8 @@ from dcs.task import ( Targets, Task, WeaponType, + AWACSTaskAction, + SetFrequencyCommand, ) from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.triggers import Event, TriggerOnce, TriggerRule @@ -88,7 +90,6 @@ from game.theater.controlpoint import ( from game.theater.theatergroundobject import TheaterGroundObject from game.unitmap import UnitMap from game.utils import Distance, meters, nautical_miles -from gen.airsupportgen import AirSupport from gen.ato import AirTaskingOrder, Package from gen.callsigns import create_group_callsign_from_unit from gen.flights.flight import ( @@ -104,9 +105,12 @@ from .flights.flightplan import ( LoiterFlightPlan, PatrollingFlightPlan, SweepFlightPlan, + AwacsFlightPlan, ) from .flights.traveltime import GroundSpeed, TotEstimator from .naming import namegen +from .airsupportgen import AirSupport, AwacsInfo +from .callsigns import callsign_for_support_unit if TYPE_CHECKING: from game import Game @@ -652,6 +656,12 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = { ), channel_namer=HueyChannelNamer, ), + "F-22A": AircraftData( + inter_flight_radio=get_radio("SCR-522"), + intra_flight_radio=get_radio("SCR-522"), + channel_allocator=None, + channel_namer=SCR522ChannelNamer, + ), } AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"] AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"] @@ -666,6 +676,7 @@ class AircraftConflictGenerator: game: Game, radio_registry: RadioRegistry, unit_map: UnitMap, + air_support: AirSupport, ) -> None: self.m = mission self.game = game @@ -673,6 +684,7 @@ class AircraftConflictGenerator: self.radio_registry = radio_registry self.unit_map = unit_map self.flights: List[FlightData] = [] + self.air_support = air_support @cached_property def use_client(self) -> bool: @@ -787,7 +799,10 @@ class AircraftConflictGenerator: OptReactOnThreat(OptReactOnThreat.Values.EvadeFire) ) - channel = self.get_intra_flight_channel(unit_type) + if flight.flight_type == FlightType.AEWC: + channel = self.radio_registry.alloc_uhf() + else: + channel = self.get_intra_flight_channel(unit_type) group.set_frequency(channel.mhz) divert = None @@ -824,6 +839,20 @@ class AircraftConflictGenerator: if unit_type in [Su_33, C_101EB, C_101CC]: self.set_reduced_fuel(flight, group, unit_type) + if isinstance(flight.flight_plan, AwacsFlightPlan): + callsign = callsign_for_support_unit(group) + + self.air_support.awacs.append( + AwacsInfo( + dcsGroupName=str(group.name), + callsign=callsign, + freq=channel, + depature_location=flight.departure.name, + end_time=flight.flight_plan.mission_departure_time, + start_time=flight.flight_plan.mission_start_time, + ) + ) + def _generate_at_airport( self, name: str, @@ -1356,7 +1385,16 @@ class AircraftConflictGenerator: dynamic_runways: Dict[str, RunwayData], ) -> None: group.task = AWACS.name + + if not isinstance(flight.flight_plan, AwacsFlightPlan): + logging.error( + f"Cannot configure AEW&C tasks for {flight} because it does not have an AEW&C flight plan." + ) + return + self._setup_group(group, AWACS, package, flight, dynamic_runways) + + # Awacs task action self.configure_behavior( group, react_on_threat=OptReactOnThreat.Values.EvadeFire, @@ -1364,6 +1402,8 @@ class AircraftConflictGenerator: restrict_jettison=True, ) + group.points[0].tasks.append(AWACSTaskAction()) + def configure_escort( self, group: FlyingGroup, diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 88520374..a0d9f75e 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -1,6 +1,7 @@ import logging from dataclasses import dataclass, field -from typing import List, Type, Tuple +from datetime import timedelta +from typing import List, Type, Tuple, Optional from dcs.mission import Mission, StartType from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135 @@ -37,6 +38,9 @@ class AwacsInfo: dcsGroupName: str callsign: str freq: RadioFrequency + depature_location: Optional[str] + start_time: Optional[timedelta] + end_time: Optional[timedelta] @dataclass @@ -192,9 +196,12 @@ class AirSupportConflictGenerator: self.air_support.awacs.append( AwacsInfo( - str(awacs_flight.name), - callsign_for_support_unit(awacs_flight), - freq, + dcsGroupName=str(awacs_flight.name), + callsign=callsign_for_support_unit(awacs_flight), + freq=freq, + depature_location=None, + start_time=None, + end_time=None, ) ) else: diff --git a/gen/briefinggen.py b/gen/briefinggen.py index b1246c78..017c4e4e 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -20,6 +20,7 @@ from .ground_forces.combat_stance import CombatStance from .radios import RadioFrequency from .runways import RunwayData + if TYPE_CHECKING: from game import Game diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index 67a38d3d..4f30474a 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -88,6 +88,7 @@ from dcs.planes import ( Tu_22M3, Tu_95MS, WingLoong_I, + I_16, ) from dcs.unittype import FlyingType diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 431135d7..2b929387 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -713,6 +713,10 @@ class AwacsFlightPlan(LoiterFlightPlan): if self.divert is not None: yield self.divert + @property + def mission_start_time(self) -> Optional[timedelta]: + return self.takeoff_time() + def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]: if waypoint == self.hold: return self.package.time_over_target @@ -796,7 +800,17 @@ class FlightPlanBuilder: raise RuntimeError("Flight must be a part of the package") if self.package.waypoints is None: self.regenerate_package_waypoints() - flight.flight_plan = self.generate_flight_plan(flight, custom_targets) + + from game.navmesh import NavMeshError + + try: + flight.flight_plan = self.generate_flight_plan(flight, custom_targets) + except NavMeshError as ex: + color = "blue" if self.is_player else "red" + raise PlanningError( + f"Could not plan {color} {flight.flight_type.value} from " + f"{flight.departure} to {flight.package.target}" + ) from ex def generate_flight_plan( self, flight: Flight, custom_targets: Optional[List[Unit]] @@ -1013,7 +1027,8 @@ class FlightPlanBuilder: targets: List[StrikeTarget] = [] for group in location.groups: - targets.append(StrikeTarget(f"{group.name} at {location.name}", group)) + if group.units: + targets.append(StrikeTarget(f"{group.name} at {location.name}", group)) return self.strike_flightplan( flight, location, FlightWaypointType.INGRESS_BAI, targets diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 69ffac81..53fad47c 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -14,7 +14,7 @@ from typing import ( from dcs.mapping import Point from dcs.unit import Unit -from dcs.unitgroup import VehicleGroup +from dcs.unitgroup import Group, VehicleGroup if TYPE_CHECKING: from game import Game @@ -32,7 +32,7 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType @dataclass(frozen=True) class StrikeTarget: name: str - target: Union[VehicleGroup, TheaterGroundObject, Unit] + target: Union[VehicleGroup, TheaterGroundObject, Unit, Group] class WaypointBuilder: diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 14b4c9d4..a99eb9a5 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -41,6 +41,7 @@ from .flights.flight import FlightWaypoint, FlightWaypointType from .radios import RadioFrequency from .runways import RunwayData + if TYPE_CHECKING: from game import Game @@ -48,8 +49,16 @@ if TYPE_CHECKING: class KneeboardPageWriter: """Creates kneeboard images.""" - def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None: - self.image = Image.new("RGB", (768, 1024), (0xFF, 0xFF, 0xFF)) + def __init__( + self, page_margin: int = 24, line_spacing: int = 12, dark_theme: bool = False + ) -> None: + if dark_theme: + self.foreground_fill = (215, 200, 200) + self.background_fill = (10, 5, 5) + else: + self.foreground_fill = (15, 15, 15) + self.background_fill = (255, 252, 252) + self.image = Image.new("RGB", (768, 1024), self.background_fill) # These font sizes create a relatively full page for current sorties. If # we start generating more complicated flight plans, or start including # more information in the comm ladder (the latter of which we should @@ -79,10 +88,10 @@ class KneeboardPageWriter: self.y += height + self.line_spacing def title(self, title: str) -> None: - self.text(title, font=self.title_font) + self.text(title, font=self.title_font, fill=self.foreground_fill) def heading(self, text: str) -> None: - self.text(text, font=self.heading_font) + self.text(text, font=self.heading_font, fill=self.foreground_fill) def table( self, cells: List[List[str]], headers: Optional[List[str]] = None @@ -90,7 +99,7 @@ class KneeboardPageWriter: if headers is None: headers = [] table = tabulate(cells, headers=headers, numalign="right") - self.text(table, font=self.table_font) + self.text(table, font=self.table_font, fill=self.foreground_fill) def write(self, path: Path) -> None: self.image.save(path) @@ -237,6 +246,7 @@ class BriefingPage(KneeboardPage): tankers: List[TankerInfo], jtacs: List[JtacInfo], start_time: datetime.datetime, + dark_kneeboard: bool, ) -> None: self.flight = flight self.comms = list(comms) @@ -244,10 +254,11 @@ class BriefingPage(KneeboardPage): self.tankers = tankers self.jtacs = jtacs self.start_time = start_time + self.dark_kneeboard = dark_kneeboard self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel)) def write(self, path: Path) -> None: - writer = KneeboardPageWriter() + writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard) if self.flight.custom_name is not None: custom_name_title = ' ("{}")'.format(self.flight.custom_name) else: @@ -285,6 +296,34 @@ class BriefingPage(KneeboardPage): ["Bingo", "Joker"], ) + # AEW&C + writer.heading("AEW&C") + aewc_ladder = [] + + for single_aewc in self.awacs: + + if single_aewc.depature_location is None: + dep = "-" + arr = "-" + else: + dep = self._format_time(single_aewc.start_time) + arr = self._format_time(single_aewc.end_time) + + aewc_ladder.append( + [ + str(single_aewc.callsign), + str(single_aewc.freq), + str(single_aewc.depature_location), + str(dep), + str(arr), + ] + ) + + writer.table( + aewc_ladder, + headers=["Callsign", "FREQ", "Depature", "ETD", "ETA"], + ) + # Package Section writer.heading("Comm ladder") comm_ladder = [] @@ -293,10 +332,6 @@ class BriefingPage(KneeboardPage): [comm.name, "", "", "", self.format_frequency(comm.freq)] ) - for a in self.awacs: - comm_ladder.append( - [a.callsign, "AWACS", "", "", self.format_frequency(a.freq)] - ) for tanker in self.tankers: comm_ladder.append( [ @@ -365,12 +400,21 @@ class BriefingPage(KneeboardPage): channel_name = namer.channel_name(channel.radio_id, channel.channel) return f"{channel_name} {frequency}" + def _format_time(self, time: Optional[datetime.timedelta]) -> str: + if time is None: + return "" + local_time = self.start_time + time + return local_time.strftime(f"%H:%M:%S") + class KneeboardGenerator(MissionInfoGenerator): """Creates kneeboard pages for each client flight in the mission.""" def __init__(self, mission: Mission, game: "Game") -> None: super().__init__(mission, game) + self.dark_kneeboard = self.game.settings.generate_dark_kneeboard and ( + self.mission.start_time.hour > 19 or self.mission.start_time.hour < 7 + ) def generate(self) -> None: """Generates a kneeboard per client flight.""" @@ -414,5 +458,6 @@ class KneeboardGenerator(MissionInfoGenerator): self.tankers, self.jtacs, self.mission.start_time, + self.dark_kneeboard, ), ] diff --git a/pydcs b/pydcs index 5ffae3c7..42de2ec3 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit 5ffae3c76b99610ab5065c7317a8a5c72c7e4afb +Subproject commit 42de2ec352903d592ca123950b4b12a15ffa6544 diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 2be5e520..d8b5f206 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -495,6 +495,7 @@ class QLiberationMap(QGraphicsView): package = Package(target) flight = Flight( package, + self.game.player_country if player else self.game.enemy_country, F_16C_50, 2, task, @@ -914,35 +915,48 @@ class QLiberationMap(QGraphicsView): SMALL_LINE = 2 dist = self.distance_to_pixels(nautical_miles(scale_distance_nm)) - self.scene().addRect( - POS_X, - POS_Y - PADDING, - PADDING * 2 + dist, - BIG_LINE * 2 + 3 * PADDING, - pen=CONST.COLORS["black"], - brush=CONST.COLORS["black"], - ) l = self.scene().addLine( POS_X + PADDING, POS_Y + BIG_LINE * 2, POS_X + PADDING + dist, POS_Y + BIG_LINE * 2, ) + l.setPen(CONST.COLORS["black"]) + + lw = self.scene().addLine( + POS_X + PADDING + 1, + POS_Y + BIG_LINE * 2 + 1, + POS_X + PADDING + dist + 1, + POS_Y + BIG_LINE * 2 + 1, + ) + lw.setPen(CONST.COLORS["white"]) text = self.scene().addText( "0nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False) ) text.setPos(POS_X, POS_Y + BIG_LINE * 2) - text.setDefaultTextColor(Qt.white) + text.setDefaultTextColor(Qt.black) + + text_white = self.scene().addText( + "0nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False) + ) + text_white.setPos(POS_X + 1, POS_Y + BIG_LINE * 2) + text_white.setDefaultTextColor(Qt.white) text2 = self.scene().addText( str(scale_distance_nm) + "nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False), ) text2.setPos(POS_X + dist, POS_Y + BIG_LINE * 2) - text2.setDefaultTextColor(Qt.white) + text2.setDefaultTextColor(Qt.black) + + text2_white = self.scene().addText( + str(scale_distance_nm) + "nm", + font=QFont("Trebuchet MS", 6, weight=5, italic=False), + ) + text2_white.setPos(POS_X + dist + 1, POS_Y + BIG_LINE * 2) + text2_white.setDefaultTextColor(Qt.white) - l.setPen(CONST.COLORS["white"]) for i in range(number_of_points + 1): d = float(i) / float(number_of_points) if i == 0 or i == number_of_points: @@ -956,7 +970,15 @@ class QLiberationMap(QGraphicsView): POS_X + PADDING + d * dist, POS_Y + BIG_LINE - h, ) - l.setPen(CONST.COLORS["white"]) + l.setPen(CONST.COLORS["black"]) + + lw = self.scene().addLine( + POS_X + PADDING + d * dist + 1, + POS_Y + BIG_LINE * 2, + POS_X + PADDING + d * dist + 1, + POS_Y + BIG_LINE - h, + ) + lw.setPen(CONST.COLORS["white"]) def wheelEvent(self, event: QWheelEvent): if event.angleDelta().y() > 0: diff --git a/qt_ui/windows/QUnitInfoWindow.py b/qt_ui/windows/QUnitInfoWindow.py index 541f208b..cd87a07f 100644 --- a/qt_ui/windows/QUnitInfoWindow.py +++ b/qt_ui/windows/QUnitInfoWindow.py @@ -48,6 +48,9 @@ class QUnitInfoWindow(QDialog): header = QLabel(self) header.setGeometry(0, 0, 720, 360) + + pixmap = None + if ( dcs.planes.plane_map.get(self.unit_type.id) is not None or dcs.helicopters.helicopter_map.get(self.unit_type.id) is not None diff --git a/qt_ui/windows/mission/flight/payload/QPylonEditor.py b/qt_ui/windows/mission/flight/payload/QPylonEditor.py index eb0314cb..8591b7d6 100644 --- a/qt_ui/windows/mission/flight/payload/QPylonEditor.py +++ b/qt_ui/windows/mission/flight/payload/QPylonEditor.py @@ -81,6 +81,10 @@ class QPylonEditor(QComboBox): ) ) else: - self.setCurrentText( - weapons_data.weapon_ids.get(pylon_default_weapon).get("name") - ) + weapon = weapons_data.weapon_ids.get(pylon_default_weapon) + if weapon is not None: + self.setCurrentText( + weapons_data.weapon_ids.get(pylon_default_weapon).get("name") + ) + else: + self.setCurrentText(pylon_default_weapon) diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index b0c85152..ceba6e1b 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -422,6 +422,12 @@ class QSettingsWindow(QDialog): self.generate_marks.setChecked(self.game.settings.generate_marks) self.generate_marks.toggled.connect(self.applySettings) + self.generate_dark_kneeboard = QCheckBox() + self.generate_dark_kneeboard.setChecked( + self.game.settings.generate_dark_kneeboard + ) + self.generate_dark_kneeboard.toggled.connect(self.applySettings) + self.never_delay_players = QCheckBox() self.never_delay_players.setChecked( self.game.settings.never_delay_player_flights @@ -437,6 +443,14 @@ class QSettingsWindow(QDialog): self.gameplayLayout.addWidget(QLabel("Put Objective Markers on Map"), 1, 0) self.gameplayLayout.addWidget(self.generate_marks, 1, 1, Qt.AlignRight) + dark_kneeboard_label = QLabel( + "Generate Dark Kneeboard
" + "Dark kneeboard for night missions.
" + "This will likely make the kneeboard on the pilot leg unreadable.
" + ) + self.gameplayLayout.addWidget(dark_kneeboard_label, 2, 0) + self.gameplayLayout.addWidget(self.generate_dark_kneeboard, 2, 1, Qt.AlignRight) + spawn_players_immediately_tooltip = ( "Always spawns player aircraft immediately, even if their start time is " "more than 10 minutes after the start of the mission. This does " @@ -449,8 +463,8 @@ class QSettingsWindow(QDialog): "Should not be used if players have runway or in-air starts." ) spawn_immediately_label.setToolTip(spawn_players_immediately_tooltip) - self.gameplayLayout.addWidget(spawn_immediately_label, 2, 0) - self.gameplayLayout.addWidget(self.never_delay_players, 2, 1, Qt.AlignRight) + self.gameplayLayout.addWidget(spawn_immediately_label, 3, 0) + self.gameplayLayout.addWidget(self.never_delay_players, 3, 1, Qt.AlignRight) start_type_label = QLabel( "Default start type for AI aircraft
Warning: " @@ -460,8 +474,8 @@ class QSettingsWindow(QDialog): start_type = StartTypeComboBox(self.game.settings) start_type.setCurrentText(self.game.settings.default_start_type) - self.gameplayLayout.addWidget(start_type_label, 3, 0) - self.gameplayLayout.addWidget(start_type, 3, 1) + self.gameplayLayout.addWidget(start_type_label, 4, 0) + self.gameplayLayout.addWidget(start_type, 4, 1) self.performance = QGroupBox("Performance") self.performanceLayout = QGridLayout() @@ -629,6 +643,10 @@ class QSettingsWindow(QDialog): self.game.settings.supercarrier = self.supercarrier.isChecked() + self.game.settings.generate_dark_kneeboard = ( + self.generate_dark_kneeboard.isChecked() + ) + self.game.settings.perf_red_alert_state = self.red_alert.isChecked() self.game.settings.perf_smoke_gen = self.smoke.isChecked() self.game.settings.perf_artillery = self.arti.isChecked() diff --git a/requirements.txt b/requirements.txt index f537f6cd..de19d500 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ mypy-extensions==0.4.3 nodeenv==1.5.0 pathspec==0.8.1 pefile==2019.4.18 -Pillow==7.2.0 +Pillow==8.1.1 pre-commit==2.10.1 PyInstaller==3.6 PySide2==5.15.2 diff --git a/resources/ui/units/aircrafts/banners/F-22A_24.jpg b/resources/ui/units/aircrafts/banners/F-22A_24.jpg new file mode 100644 index 00000000..15d6071a Binary files /dev/null and b/resources/ui/units/aircrafts/banners/F-22A_24.jpg differ diff --git a/resources/ui/units/aircrafts/icons/F-22A_24.jpg b/resources/ui/units/aircrafts/icons/F-22A_24.jpg new file mode 100644 index 00000000..22df550a Binary files /dev/null and b/resources/ui/units/aircrafts/icons/F-22A_24.jpg differ