diff --git a/game/operation/frontlineattack.py b/game/operation/frontlineattack.py index 902ff6c9..48c5965c 100644 --- a/game/operation/frontlineattack.py +++ b/game/operation/frontlineattack.py @@ -36,6 +36,4 @@ class FrontlineAttackOperation(Operation): def generate(self): self.briefinggen.title = "Frontline CAS" self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu." - self.briefinggen.append_waypoint("CAS AREA IP") - self.briefinggen.append_waypoint("CAS AREA EGRESS") super(FrontlineAttackOperation, self).generate() diff --git a/game/operation/operation.py b/game/operation/operation.py index 051df639..f87c13e3 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -266,28 +266,34 @@ class Operation: load_dcs_libe.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_dcs_libe) + self.assign_channels_to_flights() + kneeboard_generator = KneeboardGenerator(self.current_mission) - # Briefing Generation + for dynamic_runway in self.groundobjectgen.runways.values(): + self.briefinggen.add_dynamic_runway(dynamic_runway) + for tanker in self.airsupportgen.air_support.tankers: - self.briefinggen.append_frequency( - f"Tanker {tanker.callsign} ({tanker.variant})", - f"{tanker.tacan}/{tanker.freq}") + self.briefinggen.add_tanker(tanker) kneeboard_generator.add_tanker(tanker) if self.is_awacs_enabled: for awacs in self.airsupportgen.air_support.awacs: - self.briefinggen.append_frequency(awacs.callsign, awacs.freq) + self.briefinggen.add_awacs(awacs) kneeboard_generator.add_awacs(awacs) - self.assign_channels_to_flights() - - # Generate the briefing - self.briefinggen.generate() - for region, code, name in self.game.jtacs: - kneeboard_generator.add_jtac(name, region, code) - kneeboard_generator.generate(self.airgen.flights) + # TODO: Radio info? Type? + jtac = JtacInfo(name, region, code) + self.briefinggen.add_jtac(jtac) + kneeboard_generator.add_jtac(jtac) + + for flight in self.airgen.flights: + self.briefinggen.add_flight(flight) + kneeboard_generator.add_flight(flight) + + self.briefinggen.generate() + kneeboard_generator.generate() def assign_channels_to_flights(self) -> None: """Assigns preset radio channels for client flights.""" diff --git a/gen/aircraft.py b/gen/aircraft.py index 6e0c450d..02451ab5 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -148,8 +148,19 @@ class ChannelAssignment: class FlightData: """Details of a planned flight.""" - #: List of playable units in the flight. - client_units: List[FlyingUnit] + flight_type: FlightType + + #: All units in the flight. + units: List[FlyingUnit] + + #: Total number of aircraft in the flight. + size: int + + #: True if this flight belongs to the player's coalition. + friendly: bool + + #: Number of minutes after mission start the flight is set to depart. + departure_delay: int #: Arrival airport. arrival: RunwayData @@ -169,13 +180,18 @@ class FlightData: #: Map of radio frequencies to their assigned radio and channel, if any. frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] - def __init__(self, client_units: List[FlyingUnit], arrival: RunwayData, - departure: RunwayData, divert: Optional[RunwayData], - waypoints: List[FlightWaypoint], + 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) -> None: - self.client_units = client_units - self.arrival = arrival + self.flight_type = flight_type + self.units = units + self.size = size + self.friendly = friendly + self.departure_delay = departure_delay self.departure = departure + self.arrival = arrival self.divert = divert self.waypoints = waypoints self.intra_flight_channel = intra_flight_channel @@ -183,6 +199,11 @@ class FlightData: self.assign_intra_flight_channel() + @property + def client_units(self) -> List[FlyingUnit]: + """List of playable units in the flight.""" + return [u for u in self.units if u.is_human()] + def assign_intra_flight_channel(self) -> None: """Assigns a channel to the intra-flight frequency.""" if not self.client_units: @@ -200,10 +221,11 @@ class FlightData: @property def aircraft_type(self) -> FlyingType: """Returns the type of aircraft in this flight.""" - return self.client_units[0].unit_type + return self.units[0].unit_type def num_radio_channels(self, radio_id: int) -> int: """Returns the number of preset channels for the given radio.""" + # Note: pydcs only initializes the radio presets for client slots. return self.client_units[0].num_radio_channels(radio_id) def channel_for( @@ -296,11 +318,9 @@ class AircraftConflictGenerator: for unit_instance in group.units: unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type] - clients: List[FlyingUnit] = [] single_client = flight.client_count == 1 for idx in range(0, min(len(group.units), flight.client_count)): unit = group.units[idx] - clients.append(unit) if single_client: unit.set_player() else: @@ -338,7 +358,11 @@ class AircraftConflictGenerator: departure_runway = fallback_runway self.flights.append(FlightData( - client_units=clients, + flight_type=flight.flight_type, + units=group.units, + size=len(group.units), + friendly=flight.from_cp.captured, + departure_delay=flight.scheduled_in, departure=departure_runway, arrival=departure_runway, # TODO: Support for divert airfields. diff --git a/gen/airfields.py b/gen/airfields.py index fcd71760..e4c5f1fb 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -641,12 +641,13 @@ AIRFIELD_DATA = { } -@dataclass +@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 @@ -665,6 +666,7 @@ class RunwayData: airfield = AIRFIELD_DATA[airport.name] atc = airfield.atc.uhf tacan = airfield.tacan + tacan = airfield.tacan_callsign ils = airfield.ils_freq(runway) except KeyError: logging.warning(f"No airfield data for {airport.name}") diff --git a/gen/briefinggen.py b/gen/briefinggen.py index c3e10931..9eed3af2 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -1,68 +1,136 @@ -import logging +import os +from collections import defaultdict +from dataclasses import dataclass +import random +from typing import List, Tuple from game import db -from .conflictgen import * -from .naming import * - -from dcs.mission import * +from pydcs.dcs.mission import Mission +from .aircraft import FlightData +from .airfields import RunwayData +from .airsupportgen import AwacsInfo, TankerInfo +from .conflictgen import Conflict +from .ground_forces.combat_stance import CombatStance +from .radios import RadioFrequency -class BriefingGenerator: - freqs = None # type: typing.List[typing.Tuple[str, str]] - title = "" # type: str - description = "" # type: str - targets = None # type: typing.List[typing.Tuple[str, str]] - waypoints = None # type: typing.List[str] +@dataclass +class CommInfo: + """Communications information for the kneeboard.""" + name: str + freq: RadioFrequency + + +@dataclass +class JtacInfo: + """JTAC information for the kneeboard.""" + callsign: str + region: str + code: str + + +class MissionInfoGenerator: + """Base type for generators of mission information for the player. + + Examples of subtypes include briefing generators, kneeboard generators, etc. + """ + + def __init__(self, mission: Mission) -> None: + self.mission = mission + self.awacs: List[AwacsInfo] = [] + self.comms: List[CommInfo] = [] + self.flights: List[FlightData] = [] + self.jtacs: List[JtacInfo] = [] + self.tankers: List[TankerInfo] = [] + + def add_awacs(self, awacs: AwacsInfo) -> None: + """Adds an AWACS/GCI to the mission. + + Args: + awacs: AWACS information. + """ + self.awacs.append(awacs) + + def add_comm(self, name: str, freq: RadioFrequency) -> None: + """Adds communications info to the mission. + + Args: + name: Name of the radio channel. + freq: Frequency of the radio channel. + """ + self.comms.append(CommInfo(name, freq)) + + def add_flight(self, flight: FlightData) -> None: + """Adds flight info to the mission. + + Args: + flight: Flight information. + """ + self.flights.append(flight) + + def add_jtac(self, jtac: JtacInfo) -> None: + """Adds a JTAC to the mission. + + Args: + jtac: JTAC information. + """ + self.jtacs.append(jtac) + + def add_tanker(self, tanker: TankerInfo) -> None: + """Adds a tanker to the mission. + + Args: + tanker: Tanker information. + """ + self.tankers.append(tanker) + + def generate(self) -> None: + """Generates the mission information.""" + raise NotImplementedError + + +class BriefingGenerator(MissionInfoGenerator): def __init__(self, mission: Mission, conflict: Conflict, game): - self.m = mission + super().__init__(mission) self.conflict = conflict self.game = game + self.title = "" self.description = "" + self.dynamic_runways: List[RunwayData] = [] - self.freqs = [] - self.targets = [] - self.waypoints = [] + def add_dynamic_runway(self, runway: RunwayData) -> None: + """Adds a dynamically generated runway to the briefing. - self.jtacs = [] + Dynamic runways are any valid landing point that is a unit rather than a + map feature. These include carriers, ships with a helipad, and FARPs. + """ + self.dynamic_runways.append(runway) - def append_frequency(self, name: str, frequency: str): - self.freqs.append((name, frequency)) + def add_flight_description(self, flight: FlightData): + assert flight.client_units - def append_target(self, description: str, markpoint: str = None): - self.targets.append((description, markpoint)) - - def append_waypoint(self, description: str): - self.waypoints.append(description) - - def add_flight_description(self, flight): - - if flight.client_count <= 0: - return - - flight_unit_name = db.unit_type_name(flight.unit_type) + aircraft = flight.aircraft_type + flight_unit_name = db.unit_type_name(aircraft) self.description += "-" * 50 + "\n" - self.description += flight_unit_name + " x " + str(flight.count) + 2 * "\n" + self.description += f"{flight_unit_name} x {flight.size + 2}\n\n" - self.description += "#0 -- TAKEOFF : Take off from " + flight.from_cp.name + "\n" - for i, wpt in enumerate(flight.points): - self.description += "#" + str(1+i) + " -- " + wpt.name + " : " + wpt.description + "\n" - self.description += "#" + str(len(flight.points) + 1) + " -- RTB\n\n" + departure = flight.departure.airfield_name + self.description += f"#0 -- TAKEOFF : Take off from {departure}\n" + for i, wpt in enumerate(flight.waypoints): + self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n" + self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n" - group = flight.group - if group is not None: - for i, nav_target in enumerate(group.nav_target_points): - self.description += nav_target.text_comment + "\n" - self.description += "\n" - self.description += "-" * 50 + "\n" - - def add_ally_flight_description(self, flight): - if flight.client_count == 0: - flight_unit_name = db.unit_type_name(flight.unit_type) - self.description += flight.flight_type.name + " " + flight_unit_name + " x " + str(flight.count) + ", departing in " + str(flight.scheduled_in) + " minutes \n" + def add_ally_flight_description(self, flight: FlightData): + assert not flight.client_units + aircraft = flight.aircraft_type + flight_unit_name = db.unit_type_name(aircraft) + self.description += ( + f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, " + f"departing in {flight.departure_delay} minutes\n" + ) def generate(self): - self.description = "" self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n" @@ -74,52 +142,50 @@ class BriefingGenerator: self.description += "Your flights:" + "\n" self.description += "=" * 15 + "\n\n" - for planner in self.game.planners.values(): - for flight in planner.flights: + for flight in self.flights: + if flight.client_units: self.add_flight_description(flight) self.description += "\n"*2 self.description += "Planned ally flights:" + "\n" self.description += "=" * 15 + "\n" - for planner in self.game.planners.values(): - if planner.from_cp.captured and len(planner.flights) > 0: - self.description += "\nFrom " + planner.from_cp.full_name + " \n" - self.description += "-" * 50 + "\n\n" - for flight in planner.flights: - self.add_ally_flight_description(flight) + allied_flights_by_departure = defaultdict(list) + for flight in self.flights: + if not flight.client_units and flight.friendly: + name = flight.departure.airfield_name + allied_flights_by_departure[name].append(flight) + for departure, flights in allied_flights_by_departure.items(): + self.description += f"\nFrom {departure}\n" + self.description += "-" * 50 + "\n\n" + for flight in flights: + self.add_ally_flight_description(flight) - if self.freqs: + if self.comms: self.description += "\n\nComms Frequencies:\n" self.description += "=" * 15 + "\n" - for name, freq in self.freqs: - self.description += "{}: {}\n".format(name, freq) + for comm_info in self.comms: + self.description += f"{comm_info.name}: {comm_info.freq}\n" self.description += ("-" * 50) + "\n" - for cp in self.game.theater.controlpoints: - if cp.captured and cp.cptype in [ControlPointType.LHA_GROUP, ControlPointType.AIRCRAFT_CARRIER_GROUP]: - self.description += cp.name + "\n" - self.description += "RADIO : 127.5 Mhz AM\n" - self.description += "TACAN : " - self.description += str(cp.tacanN) - if cp.tacanY: - self.description += "Y" - else: - self.description += "X" - self.description += " " + str(cp.tacanI) + "\n" - - if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP and hasattr(cp, "icls"): - self.description += "ICLS Channel : " + str(cp.icls) + "\n" - self.description += "-" * 50 + "\n" + for runway in self.dynamic_runways: + self.description += f"{runway.airfield_name}\n" + self.description += f"RADIO : {runway.atc}\n" + if runway.tacan is not None: + self.description += f"TACAN : {runway.tacan} {runway.tacan_callsign}\n" + if runway.icls is not None: + self.description += f"ICLS Channel : {runway.icls}\n" + self.description += "-" * 50 + "\n" self.description += "JTACS [F-10 Menu] : \n" self.description += "===================\n\n" - for jtac in self.game.jtacs: - self.description += str(jtac[0]) + " -- Code : " + str(jtac[1]) + "\n" + for jtac in self.jtacs: + self.description += f"{jtac.region} -- Code : {jtac.code}\n" - self.m.set_description_text(self.description) + self.mission.set_description_text(self.description) - self.m.add_picture_blue(os.path.abspath("./resources/ui/splash_screen.png")) + self.mission.add_picture_blue(os.path.abspath( + "./resources/ui/splash_screen.png")) def generate_ongoing_war_text(self): diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 6a12579e..3163aa3b 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -189,6 +189,7 @@ class GroundObjectsGenerator: "N/A", atc=atc_channel, tacan=tacan, + tacan_callsign=tacan_callsign, icls=icls_channel, ) diff --git a/gen/kneeboard.py b/gen/kneeboard.py index bb7bb4da..8383b392 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -23,7 +23,6 @@ only be added per airframe, so PvP missions where each side have the same aircraft will be able to see the enemy's kneeboard for the same airframe. """ from collections import defaultdict -from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -36,6 +35,7 @@ from . import units from .aircraft import FlightData from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo +from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator from .radios import RadioFrequency @@ -95,21 +95,6 @@ class KneeboardPage: raise NotImplementedError -@dataclass -class CommInfo: - """Communications information for the kneeboard.""" - name: str - freq: RadioFrequency - - -@dataclass -class JtacInfo: - """JTAC information for the kneeboard.""" - callsign: str - region: str - code: str - - class BriefingPage(KneeboardPage): """A kneeboard page containing briefing information.""" def __init__(self, flight: FlightData, comms: List[CommInfo], @@ -208,57 +193,17 @@ class BriefingPage(KneeboardPage): return f"{channel.radio_name} Ch {channel.channel}" -class KneeboardGenerator: +class KneeboardGenerator(MissionInfoGenerator): """Creates kneeboard pages for each client flight in the mission.""" def __init__(self, mission: Mission) -> None: - self.mission = mission - self.comms: List[CommInfo] = [] - self.awacs: List[AwacsInfo] = [] - self.tankers: List[TankerInfo] = [] - self.jtacs: List[JtacInfo] = [] + super().__init__(mission) - def add_comm(self, name: str, freq: RadioFrequency) -> None: - """Adds communications info to the kneeboard. - - Args: - name: Name of the radio channel. - freq: Frequency of the radio channel. - """ - self.comms.append(CommInfo(name, freq)) - - def add_awacs(self, awacs: AwacsInfo) -> None: - """Adds an AWACS/GCI to the kneeboard. - - Args: - awacs: AWACS information. - """ - self.awacs.append(awacs) - - def add_tanker(self, tanker: TankerInfo) -> None: - """Adds a tanker to the kneeboard. - - Args: - tanker: Tanker information. - """ - self.tankers.append(tanker) - - def add_jtac(self, callsign: str, region: str, code: str) -> None: - """Adds a JTAC to the kneeboard. - - Args: - callsign: Callsign of the JTAC. - region: JTAC's area of responsibility. - code: Laser code used by the JTAC. - """ - # TODO: Radio info? Type? - self.jtacs.append(JtacInfo(callsign, region, code)) - - def generate(self, flights: List[FlightData]) -> None: + def generate(self) -> None: """Generates a kneeboard per client flight.""" temp_dir = Path("kneeboards") temp_dir.mkdir(exist_ok=True) - for aircraft, pages in self.pages_by_airframe(flights).items(): + for aircraft, pages in self.pages_by_airframe().items(): aircraft_dir = temp_dir / aircraft.id aircraft_dir.mkdir(exist_ok=True) for idx, page in enumerate(pages): @@ -266,7 +211,7 @@ class KneeboardGenerator: page.write(page_path) self.mission.add_aircraft_kneeboard(aircraft, page_path) - def pages_by_airframe(self, flights: List[FlightData]) -> Dict[FlyingType, List[KneeboardPage]]: + def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]: """Returns a list of kneeboard pages per airframe in the mission. Only client flights will be included, but because DCS does not support @@ -278,7 +223,7 @@ class KneeboardGenerator: that aircraft. """ all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list) - for flight in flights: + for flight in self.flights: if not flight.client_units: continue all_flights[flight.aircraft_type].extend(