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