Merge branch 'develop' into add-e2c-to-more-factions

This commit is contained in:
SnappyComebacks 2021-03-27 11:04:13 -06:00
commit ca7a86b6d7
22 changed files with 241 additions and 53 deletions

View File

@ -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`). - 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`). - 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):** **Version information (please complete the following information):**
- DCS Liberation [e.g. 2.3.1]: - DCS Liberation [e.g. 2.3.1]:

View File

@ -8,6 +8,9 @@ Saves from 2.4 are not compatible with 2.5.
## Fixes ## 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 # 2.4.3
## Features/Improvements ## Features/Improvements

View File

@ -180,6 +180,7 @@ from pydcs_extensions.su57.su57 import Su_57
UNITINFOTEXT_PATH = Path("./resources/units/unit_info_text.json") UNITINFOTEXT_PATH = Path("./resources/units/unit_info_text.json")
plane_map["A-4E-C"] = A_4E_C plane_map["A-4E-C"] = A_4E_C
plane_map["F-22A"] = F_22A
plane_map["MB-339PAN"] = MB_339PAN plane_map["MB-339PAN"] = MB_339PAN
plane_map["Rafale_M"] = Rafale_M plane_map["Rafale_M"] = Rafale_M
plane_map["Rafale_A_S"] = Rafale_A_S plane_map["Rafale_A_S"] = Rafale_A_S

View File

@ -21,6 +21,10 @@ from game.threatzones import ThreatZones
from game.utils import nautical_miles from game.utils import nautical_miles
class NavMeshError(RuntimeError):
pass
class NavMeshPoly: class NavMeshPoly:
def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None: def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None:
self.ident = ident self.ident = ident
@ -125,7 +129,7 @@ class NavMesh:
path.append(current.world_point) path.append(current.world_point)
previous = came_from[current] previous = came_from[current]
if previous is None: if previous is None:
raise RuntimeError( raise NavMeshError(
f"Could not reconstruct path to {destination} from {origin}" f"Could not reconstruct path to {destination} from {origin}"
) )
current = previous current = previous
@ -140,10 +144,12 @@ class NavMesh:
def shortest_path(self, origin: Point, destination: Point) -> List[Point]: def shortest_path(self, origin: Point, destination: Point) -> List[Point]:
origin_poly = self.localize(origin) origin_poly = self.localize(origin)
if origin_poly is None: 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) destination_poly = self.localize(destination)
if destination_poly is None: 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( return self._shortest_path(
NavPoint(self.dcs_to_shapely_point(origin), origin_poly), 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 # threatened airbases at the map edges have room to retreat from the
# threat without running off the navmesh. # threat without running off the navmesh.
return box(*LineString(points).bounds).buffer( return box(*LineString(points).bounds).buffer(
nautical_miles(100).meters, resolution=1 nautical_miles(200).meters, resolution=1
) )
@staticmethod @staticmethod

View File

@ -166,6 +166,7 @@ class Operation:
airgen: AircraftConflictGenerator, airgen: AircraftConflictGenerator,
): ):
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)""" """Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
gens: List[MissionInfoGenerator] = [ gens: List[MissionInfoGenerator] = [
KneeboardGenerator(cls.current_mission, cls.game), KneeboardGenerator(cls.current_mission, cls.game),
BriefingGenerator(cls.current_mission, cls.game), BriefingGenerator(cls.current_mission, cls.game),
@ -177,9 +178,8 @@ class Operation:
for tanker in airsupportgen.air_support.tankers: for tanker in airsupportgen.air_support.tankers:
gen.add_tanker(tanker) gen.add_tanker(tanker)
if cls.player_awacs_enabled: for aewc in airsupportgen.air_support.awacs:
for awacs in airsupportgen.air_support.awacs: gen.add_awacs(aewc)
gen.add_awacs(awacs)
for jtac in jtacs: for jtac in jtacs:
gen.add_jtac(jtac) gen.add_jtac(jtac)
@ -378,7 +378,9 @@ class Operation:
cls.game, cls.game,
cls.radio_registry, cls.radio_registry,
cls.unit_map, cls.unit_map,
air_support=cls.airsupportgen.air_support,
) )
cls.airgen.clear_parking_slots() cls.airgen.clear_parking_slots()
cls.airgen.generate_flights( cls.airgen.generate_flights(

View File

@ -31,6 +31,7 @@ class Settings:
automate_aircraft_reinforcements: bool = False automate_aircraft_reinforcements: bool = False
restrict_weapons_by_date: bool = False restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = False disable_legacy_aewc: bool = False
generate_dark_kneeboard: bool = False
# Performance oriented # Performance oriented
perf_red_alert_state: bool = True perf_red_alert_state: bool = True

View File

@ -15,6 +15,7 @@ from shapely.ops import nearest_points, unary_union
from game.theater import ControlPoint from game.theater import ControlPoint
from game.utils import Distance, meters, nautical_miles from game.utils import Distance, meters, nautical_miles
from gen import Conflict
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight 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 zone belongs to the player, it is the zone that will be avoided by
the enemy and vice versa. the enemy and vice versa.
""" """
airbases = [] air_threats = []
air_defenses = [] air_defenses = []
for control_point in game.theater.controlpoints: for control_point in game.theater.controlpoints:
if control_point.captured != player: if control_point.captured != player:
@ -139,7 +140,7 @@ class ThreatZones:
if control_point.runway_is_operational(): if control_point.runway_is_operational():
point = ShapelyPoint(control_point.position.x, control_point.position.y) point = ShapelyPoint(control_point.position.x, control_point.position.y)
cap_threat_range = cls.barcap_threat_range(game, control_point) 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 tgo in control_point.ground_objects:
for group in tgo.groups: for group in tgo.groups:
@ -151,8 +152,25 @@ class ThreatZones:
threat_zone = point.buffer(threat_range.meters) threat_zone = point.buffer(threat_range.meters)
air_defenses.append(threat_zone) 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( return cls(
airbases=unary_union(airbases), air_defenses=unary_union(air_defenses) airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses)
) )
@staticmethod @staticmethod

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import logging import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
from functools import cached_property from functools import cached_property
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union
@ -67,6 +67,8 @@ from dcs.task import (
Targets, Targets,
Task, Task,
WeaponType, WeaponType,
AWACSTaskAction,
SetFrequencyCommand,
) )
from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import Event, TriggerOnce, TriggerRule from dcs.triggers import Event, TriggerOnce, TriggerRule
@ -88,7 +90,6 @@ from game.theater.controlpoint import (
from game.theater.theatergroundobject import TheaterGroundObject from game.theater.theatergroundobject import TheaterGroundObject
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import Distance, meters, nautical_miles from game.utils import Distance, meters, nautical_miles
from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit from gen.callsigns import create_group_callsign_from_unit
from gen.flights.flight import ( from gen.flights.flight import (
@ -104,9 +105,12 @@ from .flights.flightplan import (
LoiterFlightPlan, LoiterFlightPlan,
PatrollingFlightPlan, PatrollingFlightPlan,
SweepFlightPlan, SweepFlightPlan,
AwacsFlightPlan,
) )
from .flights.traveltime import GroundSpeed, TotEstimator from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen from .naming import namegen
from .airsupportgen import AirSupport, AwacsInfo
from .callsigns import callsign_for_support_unit
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -652,6 +656,12 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
), ),
channel_namer=HueyChannelNamer, 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["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"] AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
@ -666,6 +676,7 @@ class AircraftConflictGenerator:
game: Game, game: Game,
radio_registry: RadioRegistry, radio_registry: RadioRegistry,
unit_map: UnitMap, unit_map: UnitMap,
air_support: AirSupport,
) -> None: ) -> None:
self.m = mission self.m = mission
self.game = game self.game = game
@ -673,6 +684,7 @@ class AircraftConflictGenerator:
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.unit_map = unit_map self.unit_map = unit_map
self.flights: List[FlightData] = [] self.flights: List[FlightData] = []
self.air_support = air_support
@cached_property @cached_property
def use_client(self) -> bool: def use_client(self) -> bool:
@ -787,7 +799,10 @@ class AircraftConflictGenerator:
OptReactOnThreat(OptReactOnThreat.Values.EvadeFire) 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) group.set_frequency(channel.mhz)
divert = None divert = None
@ -824,6 +839,20 @@ class AircraftConflictGenerator:
if unit_type in [Su_33, C_101EB, C_101CC]: if unit_type in [Su_33, C_101EB, C_101CC]:
self.set_reduced_fuel(flight, group, unit_type) 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( def _generate_at_airport(
self, self,
name: str, name: str,
@ -1356,7 +1385,16 @@ class AircraftConflictGenerator:
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
group.task = AWACS.name 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) self._setup_group(group, AWACS, package, flight, dynamic_runways)
# Awacs task action
self.configure_behavior( self.configure_behavior(
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
@ -1364,6 +1402,8 @@ class AircraftConflictGenerator:
restrict_jettison=True, restrict_jettison=True,
) )
group.points[0].tasks.append(AWACSTaskAction())
def configure_escort( def configure_escort(
self, self,
group: FlyingGroup, group: FlyingGroup,

View File

@ -1,6 +1,7 @@
import logging import logging
from dataclasses import dataclass, field 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.mission import Mission, StartType
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135 from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
@ -37,6 +38,9 @@ class AwacsInfo:
dcsGroupName: str dcsGroupName: str
callsign: str callsign: str
freq: RadioFrequency freq: RadioFrequency
depature_location: Optional[str]
start_time: Optional[timedelta]
end_time: Optional[timedelta]
@dataclass @dataclass
@ -192,9 +196,12 @@ class AirSupportConflictGenerator:
self.air_support.awacs.append( self.air_support.awacs.append(
AwacsInfo( AwacsInfo(
str(awacs_flight.name), dcsGroupName=str(awacs_flight.name),
callsign_for_support_unit(awacs_flight), callsign=callsign_for_support_unit(awacs_flight),
freq, freq=freq,
depature_location=None,
start_time=None,
end_time=None,
) )
) )
else: else:

View File

@ -20,6 +20,7 @@ from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency from .radios import RadioFrequency
from .runways import RunwayData from .runways import RunwayData
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game

View File

@ -88,6 +88,7 @@ from dcs.planes import (
Tu_22M3, Tu_22M3,
Tu_95MS, Tu_95MS,
WingLoong_I, WingLoong_I,
I_16,
) )
from dcs.unittype import FlyingType from dcs.unittype import FlyingType

View File

@ -713,6 +713,10 @@ class AwacsFlightPlan(LoiterFlightPlan):
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
@property
def mission_start_time(self) -> Optional[timedelta]:
return self.takeoff_time()
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold: if waypoint == self.hold:
return self.package.time_over_target return self.package.time_over_target
@ -796,7 +800,17 @@ class FlightPlanBuilder:
raise RuntimeError("Flight must be a part of the package") raise RuntimeError("Flight must be a part of the package")
if self.package.waypoints is None: if self.package.waypoints is None:
self.regenerate_package_waypoints() 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( def generate_flight_plan(
self, flight: Flight, custom_targets: Optional[List[Unit]] self, flight: Flight, custom_targets: Optional[List[Unit]]
@ -1013,7 +1027,8 @@ class FlightPlanBuilder:
targets: List[StrikeTarget] = [] targets: List[StrikeTarget] = []
for group in location.groups: 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( return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_BAI, targets flight, location, FlightWaypointType.INGRESS_BAI, targets

View File

@ -14,7 +14,7 @@ from typing import (
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import Group, VehicleGroup
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -32,7 +32,7 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True) @dataclass(frozen=True)
class StrikeTarget: class StrikeTarget:
name: str name: str
target: Union[VehicleGroup, TheaterGroundObject, Unit] target: Union[VehicleGroup, TheaterGroundObject, Unit, Group]
class WaypointBuilder: class WaypointBuilder:

View File

@ -41,6 +41,7 @@ from .flights.flight import FlightWaypoint, FlightWaypointType
from .radios import RadioFrequency from .radios import RadioFrequency
from .runways import RunwayData from .runways import RunwayData
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -48,8 +49,16 @@ if TYPE_CHECKING:
class KneeboardPageWriter: class KneeboardPageWriter:
"""Creates kneeboard images.""" """Creates kneeboard images."""
def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None: def __init__(
self.image = Image.new("RGB", (768, 1024), (0xFF, 0xFF, 0xFF)) 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 # These font sizes create a relatively full page for current sorties. If
# we start generating more complicated flight plans, or start including # we start generating more complicated flight plans, or start including
# more information in the comm ladder (the latter of which we should # more information in the comm ladder (the latter of which we should
@ -79,10 +88,10 @@ class KneeboardPageWriter:
self.y += height + self.line_spacing self.y += height + self.line_spacing
def title(self, title: str) -> None: 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: 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( def table(
self, cells: List[List[str]], headers: Optional[List[str]] = None self, cells: List[List[str]], headers: Optional[List[str]] = None
@ -90,7 +99,7 @@ class KneeboardPageWriter:
if headers is None: if headers is None:
headers = [] headers = []
table = tabulate(cells, headers=headers, numalign="right") 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: def write(self, path: Path) -> None:
self.image.save(path) self.image.save(path)
@ -237,6 +246,7 @@ class BriefingPage(KneeboardPage):
tankers: List[TankerInfo], tankers: List[TankerInfo],
jtacs: List[JtacInfo], jtacs: List[JtacInfo],
start_time: datetime.datetime, start_time: datetime.datetime,
dark_kneeboard: bool,
) -> None: ) -> None:
self.flight = flight self.flight = flight
self.comms = list(comms) self.comms = list(comms)
@ -244,10 +254,11 @@ class BriefingPage(KneeboardPage):
self.tankers = tankers self.tankers = tankers
self.jtacs = jtacs self.jtacs = jtacs
self.start_time = start_time self.start_time = start_time
self.dark_kneeboard = dark_kneeboard
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel)) self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
def write(self, path: Path) -> None: def write(self, path: Path) -> None:
writer = KneeboardPageWriter() writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
if self.flight.custom_name is not None: if self.flight.custom_name is not None:
custom_name_title = ' ("{}")'.format(self.flight.custom_name) custom_name_title = ' ("{}")'.format(self.flight.custom_name)
else: else:
@ -285,6 +296,34 @@ class BriefingPage(KneeboardPage):
["Bingo", "Joker"], ["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 # Package Section
writer.heading("Comm ladder") writer.heading("Comm ladder")
comm_ladder = [] comm_ladder = []
@ -293,10 +332,6 @@ class BriefingPage(KneeboardPage):
[comm.name, "", "", "", self.format_frequency(comm.freq)] [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: for tanker in self.tankers:
comm_ladder.append( comm_ladder.append(
[ [
@ -365,12 +400,21 @@ class BriefingPage(KneeboardPage):
channel_name = namer.channel_name(channel.radio_id, channel.channel) channel_name = namer.channel_name(channel.radio_id, channel.channel)
return f"{channel_name} {frequency}" 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): class KneeboardGenerator(MissionInfoGenerator):
"""Creates kneeboard pages for each client flight in the mission.""" """Creates kneeboard pages for each client flight in the mission."""
def __init__(self, mission: Mission, game: "Game") -> None: def __init__(self, mission: Mission, game: "Game") -> None:
super().__init__(mission, game) 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: def generate(self) -> None:
"""Generates a kneeboard per client flight.""" """Generates a kneeboard per client flight."""
@ -414,5 +458,6 @@ class KneeboardGenerator(MissionInfoGenerator):
self.tankers, self.tankers,
self.jtacs, self.jtacs,
self.mission.start_time, self.mission.start_time,
self.dark_kneeboard,
), ),
] ]

2
pydcs

@ -1 +1 @@
Subproject commit 5ffae3c76b99610ab5065c7317a8a5c72c7e4afb Subproject commit 42de2ec352903d592ca123950b4b12a15ffa6544

View File

@ -495,6 +495,7 @@ class QLiberationMap(QGraphicsView):
package = Package(target) package = Package(target)
flight = Flight( flight = Flight(
package, package,
self.game.player_country if player else self.game.enemy_country,
F_16C_50, F_16C_50,
2, 2,
task, task,
@ -914,35 +915,48 @@ class QLiberationMap(QGraphicsView):
SMALL_LINE = 2 SMALL_LINE = 2
dist = self.distance_to_pixels(nautical_miles(scale_distance_nm)) 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( l = self.scene().addLine(
POS_X + PADDING, POS_X + PADDING,
POS_Y + BIG_LINE * 2, POS_Y + BIG_LINE * 2,
POS_X + PADDING + dist, POS_X + PADDING + dist,
POS_Y + BIG_LINE * 2, 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( text = self.scene().addText(
"0nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False) "0nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False)
) )
text.setPos(POS_X, POS_Y + BIG_LINE * 2) 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( text2 = self.scene().addText(
str(scale_distance_nm) + "nm", str(scale_distance_nm) + "nm",
font=QFont("Trebuchet MS", 6, weight=5, italic=False), font=QFont("Trebuchet MS", 6, weight=5, italic=False),
) )
text2.setPos(POS_X + dist, POS_Y + BIG_LINE * 2) 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): for i in range(number_of_points + 1):
d = float(i) / float(number_of_points) d = float(i) / float(number_of_points)
if i == 0 or i == number_of_points: if i == 0 or i == number_of_points:
@ -956,7 +970,15 @@ class QLiberationMap(QGraphicsView):
POS_X + PADDING + d * dist, POS_X + PADDING + d * dist,
POS_Y + BIG_LINE - h, 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): def wheelEvent(self, event: QWheelEvent):
if event.angleDelta().y() > 0: if event.angleDelta().y() > 0:

View File

@ -48,6 +48,9 @@ class QUnitInfoWindow(QDialog):
header = QLabel(self) header = QLabel(self)
header.setGeometry(0, 0, 720, 360) header.setGeometry(0, 0, 720, 360)
pixmap = None
if ( if (
dcs.planes.plane_map.get(self.unit_type.id) is not None 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 or dcs.helicopters.helicopter_map.get(self.unit_type.id) is not None

View File

@ -81,6 +81,10 @@ class QPylonEditor(QComboBox):
) )
) )
else: else:
self.setCurrentText( weapon = weapons_data.weapon_ids.get(pylon_default_weapon)
weapons_data.weapon_ids.get(pylon_default_weapon).get("name") if weapon is not None:
) self.setCurrentText(
weapons_data.weapon_ids.get(pylon_default_weapon).get("name")
)
else:
self.setCurrentText(pylon_default_weapon)

View File

@ -422,6 +422,12 @@ class QSettingsWindow(QDialog):
self.generate_marks.setChecked(self.game.settings.generate_marks) self.generate_marks.setChecked(self.game.settings.generate_marks)
self.generate_marks.toggled.connect(self.applySettings) 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 = QCheckBox()
self.never_delay_players.setChecked( self.never_delay_players.setChecked(
self.game.settings.never_delay_player_flights 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(QLabel("Put Objective Markers on Map"), 1, 0)
self.gameplayLayout.addWidget(self.generate_marks, 1, 1, Qt.AlignRight) self.gameplayLayout.addWidget(self.generate_marks, 1, 1, Qt.AlignRight)
dark_kneeboard_label = QLabel(
"Generate Dark Kneeboard <br />"
"<strong>Dark kneeboard for night missions.<br />"
"This will likely make the kneeboard on the pilot leg unreadable.</strong>"
)
self.gameplayLayout.addWidget(dark_kneeboard_label, 2, 0)
self.gameplayLayout.addWidget(self.generate_dark_kneeboard, 2, 1, Qt.AlignRight)
spawn_players_immediately_tooltip = ( spawn_players_immediately_tooltip = (
"Always spawns player aircraft immediately, even if their start time is " "Always spawns player aircraft immediately, even if their start time is "
"more than 10 minutes after the start of the mission. <strong>This does " "more than 10 minutes after the start of the mission. <strong>This does "
@ -449,8 +463,8 @@ class QSettingsWindow(QDialog):
"Should not be used if players have runway or in-air starts.</strong>" "Should not be used if players have runway or in-air starts.</strong>"
) )
spawn_immediately_label.setToolTip(spawn_players_immediately_tooltip) spawn_immediately_label.setToolTip(spawn_players_immediately_tooltip)
self.gameplayLayout.addWidget(spawn_immediately_label, 2, 0) self.gameplayLayout.addWidget(spawn_immediately_label, 3, 0)
self.gameplayLayout.addWidget(self.never_delay_players, 2, 1, Qt.AlignRight) self.gameplayLayout.addWidget(self.never_delay_players, 3, 1, Qt.AlignRight)
start_type_label = QLabel( start_type_label = QLabel(
"Default start type for AI aircraft<br /><strong>Warning: " "Default start type for AI aircraft<br /><strong>Warning: "
@ -460,8 +474,8 @@ class QSettingsWindow(QDialog):
start_type = StartTypeComboBox(self.game.settings) start_type = StartTypeComboBox(self.game.settings)
start_type.setCurrentText(self.game.settings.default_start_type) start_type.setCurrentText(self.game.settings.default_start_type)
self.gameplayLayout.addWidget(start_type_label, 3, 0) self.gameplayLayout.addWidget(start_type_label, 4, 0)
self.gameplayLayout.addWidget(start_type, 3, 1) self.gameplayLayout.addWidget(start_type, 4, 1)
self.performance = QGroupBox("Performance") self.performance = QGroupBox("Performance")
self.performanceLayout = QGridLayout() self.performanceLayout = QGridLayout()
@ -629,6 +643,10 @@ class QSettingsWindow(QDialog):
self.game.settings.supercarrier = self.supercarrier.isChecked() 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_red_alert_state = self.red_alert.isChecked()
self.game.settings.perf_smoke_gen = self.smoke.isChecked() self.game.settings.perf_smoke_gen = self.smoke.isChecked()
self.game.settings.perf_artillery = self.arti.isChecked() self.game.settings.perf_artillery = self.arti.isChecked()

View File

@ -14,7 +14,7 @@ mypy-extensions==0.4.3
nodeenv==1.5.0 nodeenv==1.5.0
pathspec==0.8.1 pathspec==0.8.1
pefile==2019.4.18 pefile==2019.4.18
Pillow==7.2.0 Pillow==8.1.1
pre-commit==2.10.1 pre-commit==2.10.1
PyInstaller==3.6 PyInstaller==3.6
PySide2==5.15.2 PySide2==5.15.2

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB