diff --git a/game/operation/operation.py b/game/operation/operation.py index 09b5eb0a..051df639 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -80,7 +80,13 @@ class Operation: self.visualgen = VisualGenerator(mission, conflict, self.game) self.envgen = EnviromentGenerator(mission, conflict, self.game) self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game) - self.groundobjectgen = GroundObjectsGenerator(mission, conflict, self.game) + self.groundobjectgen = GroundObjectsGenerator( + mission, + conflict, + self.game, + self.radio_registry, + self.tacan_registry + ) self.briefinggen = BriefingGenerator(mission, conflict, self.game) def prepare(self, terrain: Terrain, is_quick: bool): @@ -136,15 +142,6 @@ class Operation: for frequency in unique_beacon_frequencies: self.radio_registry.reserve(frequency) - # Generate meteo - if self.environment_settings is None: - self.environment_settings = self.envgen.generate() - else: - self.envgen.load(self.environment_settings) - - # Generate ground object first - self.groundobjectgen.generate() - for airfield, data in AIRFIELD_DATA.items(): if data.theater == self.game.theater.terrain.name: self.radio_registry.reserve(data.atc.hf) @@ -154,6 +151,15 @@ class Operation: # No need to reserve ILS or TACAN because those are in the # beacon list. + # Generate meteo + if self.environment_settings is None: + self.environment_settings = self.envgen.generate() + else: + self.envgen.load(self.environment_settings) + + # Generate ground object first + self.groundobjectgen.generate() + # Generate destroyed units for d in self.game.get_destroyed_units(): try: @@ -185,7 +191,12 @@ class Operation: else: country = self.current_mission.country(self.game.enemy_country) if cp.id in self.game.planners.keys(): - self.airgen.generate_flights(cp, country, self.game.planners[cp.id]) + self.airgen.generate_flights( + cp, + country, + self.game.planners[cp.id], + self.groundobjectgen.runways + ) # Generate ground units on frontline everywhere self.game.jtacs = [] @@ -309,27 +320,16 @@ class Operation: last_channel = flight.num_radio_channels(radio_id) channel_alloc = iter(range(first_channel, last_channel + 1)) - # TODO: Fix departure/arrival to support carriers. - if flight.departure is not None: - try: - departure = AIRFIELD_DATA[flight.departure.name] - flight.assign_channel( - radio_id, next(channel_alloc), departure.atc.uhf) - except KeyError: - pass + flight.assign_channel(radio_id, next(channel_alloc),flight.departure.atc) # TODO: If there ever are multiple AWACS, limit to mission relevant. for awacs in self.airsupportgen.air_support.awacs: flight.assign_channel(radio_id, next(channel_alloc), awacs.freq) # TODO: Fix departure/arrival to support carriers. - if flight.arrival is not None and flight.arrival != flight.departure: - try: - arrival = AIRFIELD_DATA[flight.arrival.name] - flight.assign_channel( - radio_id, next(channel_alloc), arrival.atc.uhf) - except KeyError: - pass + if flight.arrival != flight.departure: + flight.assign_channel(radio_id, next(channel_alloc), + flight.arrival.atc) try: # TODO: Skip incompatible tankers. @@ -338,12 +338,8 @@ class Operation: radio_id, next(channel_alloc), tanker.freq) if flight.divert is not None: - try: - divert = AIRFIELD_DATA[flight.divert.name] - flight.assign_channel( - radio_id, next(channel_alloc), divert.atc.uhf) - except KeyError: - pass + flight.assign_channel(radio_id, next(channel_alloc), + flight.divert.atc) except StopIteration: # Any remaining channels are nice-to-haves, but not necessary for # the few aircraft with a small number of channels available. diff --git a/gen/aircraft.py b/gen/aircraft.py index e0e8460e..941bcd38 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -4,6 +4,7 @@ from typing import Dict, List, Optional, Tuple from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter +from gen.airfields import RunwayData from gen.flights.ai_flight_planner import FlightPlanner from gen.flights.flight import ( Flight, @@ -150,15 +151,14 @@ class FlightData: #: List of playable units in the flight. client_units: List[FlyingUnit] - # TODO: Arrival and departure should not be optional, but carriers don't count. #: Arrival airport. - arrival: Optional[Airport] + arrival: RunwayData #: Departure airport. - departure: Optional[Airport] + departure: RunwayData #: Diver airport. - divert: Optional[Airport] + divert: Optional[RunwayData] #: Waypoints of the flight plan. waypoints: List[FlightWaypoint] @@ -169,8 +169,8 @@ 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: Airport, - departure: Airport, divert: Optional[Airport], + def __init__(self, client_units: List[FlyingUnit], arrival: RunwayData, + departure: RunwayData, divert: Optional[RunwayData], waypoints: List[FlightWaypoint], intra_flight_channel: RadioFrequency) -> None: self.client_units = client_units @@ -261,8 +261,8 @@ class AircraftConflictGenerator: def _start_type(self) -> StartType: return self.settings.cold_start and StartType.Cold or StartType.Warm - - def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], flight: Flight): + def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], + flight: Flight, dynamic_runways: Dict[str, RunwayData]): did_load_loadout = False unit_type = group.units[0].unit_type @@ -319,10 +319,28 @@ class AircraftConflictGenerator: radio_id, channel = self.get_intra_flight_channel(unit_type) group.set_frequency(channel.mhz, radio_id) + + # TODO: Support for different departure/arrival airfields. + cp = flight.from_cp + fallback_runway = RunwayData(cp.full_name, runway_name="") + if cp.cptype == ControlPointType.AIRBASE: + # TODO: Implement logic for picking preferred runway. + runway = flight.from_cp.airport.runways[0] + runway_side = ["", "L", "R"][runway.leftright] + runway_name = f"{runway.heading}{runway_side}" + departure_runway = RunwayData.for_airfield( + flight.from_cp.airport, runway_name) + elif cp.is_fleet: + departure_runway = dynamic_runways.get(cp.name, fallback_runway) + else: + logging.warning(f"Unhandled departure control point: {cp.cptype}") + departure_runway = fallback_runway + self.flights.append(FlightData( client_units=clients, - departure=flight.from_cp.airport, - arrival=flight.from_cp.airport, + departure=departure_runway, + arrival=departure_runway, + # TODO: Support for divert airfields. divert=None, waypoints=flight.points, intra_flight_channel=channel @@ -477,8 +495,8 @@ class AircraftConflictGenerator: logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type)) - def generate_flights(self, cp, country, flight_planner:FlightPlanner): - + def generate_flights(self, cp, country, flight_planner: FlightPlanner, + dynamic_runways: Dict[str, RunwayData]): # Clear pydcs parking slots if cp.airport is not None: logging.info("CLEARING SLOTS @ " + cp.airport.name) @@ -497,7 +515,8 @@ class AircraftConflictGenerator: continue logging.info("Generating flight : " + str(flight.unit_type)) group = self.generate_planned_flight(cp, country, flight) - self.setup_flight_group(group, flight, flight.flight_type) + self.setup_flight_group(group, flight, flight.flight_type, + dynamic_runways) self.setup_group_activation_trigger(flight, group) @@ -608,19 +627,13 @@ class AircraftConflictGenerator: flight.group = group return group - def setup_group_as_intercept_flight(self, group, flight): - group.points[0].ETA = 0 - group.late_activation = True - self._setup_group(group, Intercept, flight) - for point in flight.points: - group.add_waypoint(Point(point.x,point.y), point.alt) - - def setup_flight_group(self, group, flight, flight_type): + def setup_flight_group(self, group, flight, flight_type, + dynamic_runways: Dict[str, RunwayData]): if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION]: group.task = CAP.name - self._setup_group(group, CAP, flight) + self._setup_group(group, CAP, flight, dynamic_runways) # group.points[0].tasks.clear() group.points[0].tasks.clear() group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), targets=[Targets.All.Air])) @@ -632,7 +645,7 @@ class AircraftConflictGenerator: elif flight_type in [FlightType.CAS, FlightType.BAI]: group.task = CAS.name - self._setup_group(group, CAS, flight) + self._setup_group(group, CAS, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(10), targets=[Targets.All.GroundUnits.GroundVehicles])) group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) @@ -641,7 +654,7 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptRestrictJettison(True)) elif flight_type in [FlightType.SEAD, FlightType.DEAD]: group.task = SEAD.name - self._setup_group(group, SEAD, flight) + self._setup_group(group, SEAD, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(NoTask()) group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) @@ -650,14 +663,14 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.ASM)) elif flight_type in [FlightType.STRIKE]: group.task = PinpointStrike.name - self._setup_group(group, GroundAttack, flight) + self._setup_group(group, GroundAttack, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) group.points[0].tasks.append(OptRestrictJettison(True)) elif flight_type in [FlightType.ANTISHIP]: group.task = AntishipStrike.name - self._setup_group(group, AntishipStrike, flight) + self._setup_group(group, AntishipStrike, flight, dynamic_runways) group.points[0].tasks.clear() group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) @@ -736,23 +749,3 @@ class AircraftConflictGenerator: pt.name = String(point.name) self._setup_custom_payload(flight, group) - - - def setup_group_as_antiship_flight(self, group, flight): - group.task = AntishipStrike.name - self._setup_group(group, AntishipStrike, flight) - - group.points[0].tasks.clear() - group.points[0].tasks.append(AntishipStrikeTaskAction()) - group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) - group.points[0].tasks.append(OptROE(OptROE.Values.OpenFireWeaponFree)) - group.points[0].tasks.append(OptRestrictJettison(True)) - - for point in flight.points: - group.add_waypoint(Point(point.x, point.y), point.alt) - - - def setup_radio_preset(self, flight, group): - pass - - diff --git a/gen/airfields.py b/gen/airfields.py index 8b98668d..12680c40 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -4,8 +4,10 @@ Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing data added to pydcs. Until then, missing data can be manually filled in here. """ from dataclasses import dataclass, field +import logging from typing import Dict, Optional, Tuple +from pydcs.dcs.terrain.terrain import Airport from .radios import MHz, RadioFrequency from .tacan import TacanBand, TacanChannel @@ -637,3 +639,39 @@ AIRFIELD_DATA = { atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), ), } + + +@dataclass +class RunwayData: + airfield_name: str + runway_name: str + atc: Optional[RadioFrequency] = None + tacan: Optional[TacanChannel] = None + ils: Optional[RadioFrequency] = None + icls: Optional[int] = None + + @classmethod + def for_airfield(cls, airport: Airport, runway: str) -> "RunwayData": + """Creates RunwayData for the given runway of an airfield. + + Args: + airport: The airfield the runway belongs to. + runway: Identifier of the runway to use. e.g. "030" or "200L". + """ + atc: Optional[RadioFrequency] = None + tacan: Optional[TacanChannel] = None + ils: Optional[RadioFrequency] = None + try: + airfield = AIRFIELD_DATA[airport.name] + atc = airfield.atc.uhf + tacan = airfield.tacan + ils = airfield.ils_freq(runway) + except KeyError: + logging.warning(f"No airfield data for {airport.name}") + return cls( + airport.name, + runway, + atc, + tacan, + ils + ) diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 89a09293..6a12579e 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -1,13 +1,19 @@ -import logging - -from game import db from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS from game.db import unit_type_from_name +from pydcs.dcs.mission import * +from pydcs.dcs.statics import * +from pydcs.dcs.task import ( + ActivateBeaconCommand, + ActivateICLSCommand, + OptAlarmState, +) +from pydcs.dcs.unit import Ship, Vehicle +from pydcs.dcs.unitgroup import StaticGroup +from .airfields import RunwayData from .conflictgen import * from .naming import * - -from dcs.mission import * -from dcs.statics import * +from .radios import RadioRegistry +from .tacan import TacanBand, TacanRegistry FARP_FRONTLINE_DISTANCE = 10000 AA_CP_MIN_DISTANCE = 40000 @@ -16,10 +22,15 @@ AA_CP_MIN_DISTANCE = 40000 class GroundObjectsGenerator: FARP_CAPACITY = 4 - def __init__(self, mission: Mission, conflict: Conflict, game): + def __init__(self, mission: Mission, conflict: Conflict, game, + radio_registry: RadioRegistry, tacan_registry: TacanRegistry): self.m = mission self.conflict = conflict self.game = game + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry + self.icls_alloc = iter(range(1, 21)) + self.runways: Dict[str, RunwayData] = {} def generate_farps(self, number_of_units=1) -> typing.Collection[StaticGroup]: if self.conflict.is_vector: @@ -103,6 +114,8 @@ class GroundObjectsGenerator: utype = db.upgrade_to_supercarrier(utype, cp.name) sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading) + atc_channel = self.radio_registry.alloc_uhf() + sg.set_frequency(atc_channel.hertz) sg.units[0].name = self.m.string(g.units[0].name) for i, u in enumerate(g.units): @@ -111,6 +124,8 @@ class GroundObjectsGenerator: ship.position.x = u.position.x ship.position.y = u.position.y ship.heading = u.heading + # TODO: Verify. + ship.set_frequency(atc_channel.hertz) sg.add_unit(ship) # Find carrier direction (In the wind) @@ -125,10 +140,57 @@ class GroundObjectsGenerator: attempt = attempt + 1 # Set UP TACAN and ICLS - modeChannel = "X" if not cp.tacanY else "Y" - sg.points[0].tasks.append(ActivateBeaconCommand(channel=cp.tacanN, modechannel=modeChannel, callsign=cp.tacanI, unit_id=sg.units[0].id, aa=False)) - if ground_object.dcs_identifier == "CARRIER" and hasattr(cp, "icls"): - sg.points[0].tasks.append(ActivateICLSCommand(cp.icls, unit_id=sg.units[0].id)) + tacan = self.tacan_registry.alloc_for_band(TacanBand.X) + icls_channel = next(self.icls_alloc) + # TODO: Assign these properly. + if ground_object.dcs_identifier == "CARRIER": + tacan_callsign = random.choice([ + "STE", + "CVN", + "CVH", + "CCV", + "ACC", + "ARC", + "GER", + "ABR", + "LIN", + "TRU", + ]) + else: + tacan_callsign = random.choice([ + "LHD", + "LHA", + "LHB", + "LHC", + "LHD", + "LDS", + ]) + sg.points[0].tasks.append(ActivateBeaconCommand( + channel=tacan.number, + modechannel=tacan.band.value, + callsign=tacan_callsign, + unit_id=sg.units[0].id, + aa=False + )) + sg.points[0].tasks.append(ActivateICLSCommand( + icls_channel, + unit_id=sg.units[0].id + )) + # TODO: Make unit name usable. + # This relies on one control point mapping exactly + # to one LHA, carrier, or other usable "runway". + # This isn't wholly true, since the DD escorts of + # the carrier group are valid for helicopters, but + # they aren't exposed as such to the game. Should + # clean this up so that's possible. We can't use the + # unit name since it's an arbitrary ID. + self.runways[cp.name] = RunwayData( + cp.name, + "N/A", + atc=atc_channel, + tacan=tacan, + icls=icls_channel, + ) else: diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 4c652757..bb7bb4da 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -31,11 +31,10 @@ from PIL import Image, ImageDraw, ImageFont from tabulate import tabulate from pydcs.dcs.mission import Mission -from pydcs.dcs.terrain.terrain import Airport from pydcs.dcs.unittype import FlyingType from . import units from .aircraft import FlightData -from .airfields import AIRFIELD_DATA +from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo from .radios import RadioFrequency @@ -135,7 +134,7 @@ class BriefingPage(KneeboardPage): self.airfield_info_row("Departure", self.flight.departure), self.airfield_info_row("Arrival", self.flight.arrival), self.airfield_info_row("Divert", self.flight.divert), - ], headers=["", "Airbase", "ATC", "TCN", "ILS", "RWY"]) + ], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"]) writer.heading("Flight Plan") flight_plan = [] @@ -176,41 +175,30 @@ class BriefingPage(KneeboardPage): writer.write(path) def airfield_info_row(self, row_title: str, - airfield: Optional[Airport]) -> List[str]: + runway: Optional[RunwayData]) -> List[str]: """Creates a table row for a given airfield. Args: row_title: Purpose of the airfield. e.g. "Departure", "Arrival" or "Divert". - airfield: The airfield described by this row. + runway: The runway described by this row. Returns: A list of strings to be used as a row of the airfield table. """ - if airfield is None: + if runway is None: return [row_title, "", "", "", "", ""] - # TODO: Implement logic for picking preferred runway. - runway = airfield.runways[0] - runway_side = ["", "L", "R"][runway.leftright] - runway_text = f"{runway.heading}{runway_side}" - - try: - extra_data = AIRFIELD_DATA[airfield.name] - atc = self.format_frequency(extra_data.atc.uhf) - tacan = extra_data.tacan or "" - ils = extra_data.ils_freq(runway) or "" - except KeyError: - atc = "" - ils = "" - tacan = "" + atc = "" + if runway.atc is not None: + atc = self.format_frequency(runway.atc) return [ row_title, - airfield.name, + runway.airfield_name, atc, - tacan, - ils, - runway_text, + runway.tacan or "", + runway.ils or runway.icls or "", + runway.runway_name, ] def format_frequency(self, frequency: RadioFrequency) -> str: diff --git a/resources/tools/generate_loadout_check.py b/resources/tools/generate_loadout_check.py index ae299a0f..165da58f 100644 --- a/resources/tools/generate_loadout_check.py +++ b/resources/tools/generate_loadout_check.py @@ -30,6 +30,6 @@ for t, uts in db.UNIT_BY_TASK.items(): altitude=10000 ) g.task = t.name - airgen._setup_group(g, t, 0) + airgen._setup_group(g, t, 0, {}) mis.save("loadout_test.miz") diff --git a/theater/controlpoint.py b/theater/controlpoint.py index 96b6605c..d7a726e7 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -27,7 +27,6 @@ class ControlPoint: full_name = None # type: str base = None # type: theater.base.Base at = None # type: db.StartPosition - icls = 1 allow_sea_units = True connected_points = None # type: typing.List[ControlPoint] @@ -38,7 +37,6 @@ class ControlPoint: frontline_offset = 0.0 cptype: ControlPointType = None - ICLS_counter = 1 alt = 0 def __init__(self, id: int, name: str, position: Point, at, radials: typing.Collection[int], size: int, importance: float, @@ -63,10 +61,6 @@ class ControlPoint: self.base = theater.base.Base() self.cptype = cptype self.stances = {} - self.tacanY = False - self.tacanN = None - self.tacanI = "TAC" - self.icls = 0 self.airport = None @classmethod @@ -81,11 +75,6 @@ class ControlPoint: import theater.conflicttheater cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1, has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP) - cp.tacanY = False - cp.tacanN = random.randint(26, 49) - cp.tacanI = random.choice(["STE", "CVN", "CVH", "CCV", "ACC", "ARC", "GER", "ABR", "LIN", "TRU"]) - ControlPoint.ICLS_counter = ControlPoint.ICLS_counter + 1 - cp.icls = ControlPoint.ICLS_counter return cp @classmethod @@ -93,9 +82,6 @@ class ControlPoint: import theater.conflicttheater cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1, has_frontline=False, cptype=ControlPointType.LHA_GROUP) - cp.tacanY = False - cp.tacanN = random.randint(1,25) - cp.tacanI = random.choice(["LHD", "LHA", "LHB", "LHC", "LHD", "LDS"]) return cp @property