diff --git a/game/operation/operation.py b/game/operation/operation.py index 7e75a305..93ad5765 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -199,7 +199,7 @@ class Operation: ) # Generate ground units on frontline everywhere - self.game.jtacs = [] + jtacs: List[JtacInfo] = [] for player_cp, enemy_cp in self.game.theater.conflicts(True): conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name, self.current_mission.country(self.attacker_country), @@ -210,6 +210,7 @@ class Operation: enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id] groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id]) groundConflictGen.generate() + jtacs.extend(groundConflictGen.jtacs) # Setup combined arms parameters self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0 @@ -250,8 +251,8 @@ class Operation: if not self.game.settings.jtac_smoke_on: smoke = "false" - for jtac in self.game.jtacs: - script = script + "\n" + "JTACAutoLase('" + str(jtac[2]) + "', " + str(jtac[1]) + ", " + smoke + ", \"vehicle\")" + "\n" + for jtac in jtacs: + script += f"\nJTACAutoLase('{jtac.unit_name}', {jtac.code}, {smoke}, 'vehicle')\n" load_autolase.add_action(DoScript(String(script))) self.current_mission.triggerrules.triggers.append(load_autolase) @@ -282,9 +283,7 @@ class Operation: self.briefinggen.add_awacs(awacs) kneeboard_generator.add_awacs(awacs) - for region, code, name in self.game.jtacs: - # TODO: Radio info? Type? - jtac = JtacInfo(name, region, code) + for jtac in jtacs: self.briefinggen.add_jtac(jtac) kneeboard_generator.add_jtac(jtac) diff --git a/gen/aircraft.py b/gen/aircraft.py index 82062b6e..7b9858c5 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -202,9 +202,27 @@ class FlightData: self.waypoints = waypoints self.intra_flight_channel = intra_flight_channel self.frequency_to_channel_map = {} + self.callsign = self.create_group_callsign() self.assign_intra_flight_channel() + def create_group_callsign(self) -> str: + lead = self.units[0] + raw_callsign = lead.callsign_as_str() + if not lead.callsign_is_western: + # Callsigns for non-Western countries are just a number per flight, + # similar to tail numbers. + return f"Flight {raw_callsign}" + + # Callsign from pydcs is in the format ``, + # where unit ID is guaranteed to be a single digit but the group ID may + # be more. + match = re.search(r"^(\D+)(\d+)(\d)$", raw_callsign) + if match is None: + logging.error(f"Could not parse unit callsign: {raw_callsign}") + return f"Flight {raw_callsign}" + return f"{match.group(1)} {match.group(2)}" + @property def client_units(self) -> List[FlyingUnit]: """List of playable units in the flight.""" @@ -254,6 +272,17 @@ class FlightData: ) +def callsign_for_support_unit(group: FlyingGroup) -> str: + # Either something like Overlord11 for Western AWACS, or else just a number. + # Convert to either "Overlord" or "Flight 123". + lead = group.units[0] + raw_callsign = lead.callsign_as_str() + try: + return f"Flight {int(raw_callsign)}" + except ValueError: + return raw_callsign.rstrip("1234567890") + + class AircraftConflictGenerator: escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]] diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index e65b5a7a..5c1d830f 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -1,16 +1,11 @@ -from typing import List from dataclasses import dataclass, field +from .aircraft import callsign_for_support_unit from .conflictgen import * from .naming import * from .radios import RadioFrequency, RadioRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry -from dcs.mission import * -from dcs.unitgroup import * -from dcs.unittype import * -from dcs.task import * - TANKER_DISTANCE = 15000 TANKER_ALT = 4572 TANKER_HEADING_OFFSET = 45 @@ -18,27 +13,6 @@ TANKER_HEADING_OFFSET = 45 AWACS_DISTANCE = 150000 AWACS_ALT = 13000 -AWACS_CALLSIGNS = [ - "Overlord", - "Magic", - "Wizard", - "Focus", - "Darkstar", -] - - -@dataclass -class TankerCallsign: - full: str - short: str - - -TANKER_CALLSIGNS = [ - TankerCallsign("Texaco", "TEX"), - TankerCallsign("Arco", "ARC"), - TankerCallsign("Shell", "SHL"), -] - @dataclass class AwacsInfo: @@ -81,8 +55,9 @@ class AirSupportConflictGenerator: def generate(self, is_awacs_enabled): player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp + fallback_tanker_number = 0 + for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)): - callsign = TANKER_CALLSIGNS[i] variant = db.unit_type_name(tanker_unit_type) freq = self.radio_registry.alloc_uhf() tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) @@ -102,19 +77,35 @@ class AirSupportConflictGenerator: tacanchannel=str(tacan), ) + callsign = callsign_for_support_unit(tanker_group) + tacan_callsign = { + "Texaco": "TEX", + "Arco": "ARC", + "Shell": "SHL", + }.get(callsign) + if tacan_callsign is None: + # The dict above is all the callsigns currently in the game, but + # non-Western countries don't use the callsigns and instead just + # use numbers. It's possible that none of those nations have + # TACAN compatible refueling aircraft, but fallback just in + # case. + tacan_callsign = f"TK{fallback_tanker_number}" + fallback_tanker_number += 1 + if tanker_unit_type != IL_78M: - tanker_group.points[0].tasks.pop() # Override PyDCS tacan channel + # Override PyDCS tacan channel. + tanker_group.points[0].tasks.pop() tanker_group.points[0].tasks.append(ActivateBeaconCommand( - tacan.number, tacan.band.value, callsign.short, True, tanker_group.units[0].id, True)) + tacan.number, tacan.band.value, tacan_callsign, True, + tanker_group.units[0].id, True)) tanker_group.points[0].tasks.append(SetInvisibleCommand(True)) tanker_group.points[0].tasks.append(SetImmortalCommand(True)) - self.air_support.tankers.append(TankerInfo(callsign.full, variant, freq, tacan)) + self.air_support.tankers.append(TankerInfo(callsign, variant, freq, tacan)) if is_awacs_enabled: try: - callsign = AWACS_CALLSIGNS[0] freq = self.radio_registry.alloc_uhf() awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0] awacs_flight = self.mission.awacs_flight( @@ -129,7 +120,8 @@ class AirSupportConflictGenerator: ) awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) - self.air_support.awacs.append(AwacsInfo(callsign, freq)) - except: - print("No AWACS for faction") + self.air_support.awacs.append(AwacsInfo( + callsign_for_support_unit(awacs_flight), freq)) + except: + print("No AWACS for faction") \ No newline at end of file diff --git a/gen/armor.py b/gen/armor.py index 5d9a8b29..82ee2f5e 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -1,10 +1,12 @@ -from dcs.action import AITaskPush, AITaskSet +from dataclasses import dataclass + +from dcs.action import AITaskPush from dcs.condition import TimeAfter, UnitDamaged, Or, GroupLifeLess -from dcs.task import * from dcs.triggers import TriggerOnce, Event from gen import namegen from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE +from .aircraft import callsign_for_support_unit from .conflictgen import * SPREAD_DISTANCE_FACTOR = 0.1, 0.3 @@ -22,6 +24,17 @@ FIGHT_DISTANCE = 3500 RANDOM_OFFSET_ATTACK = 250 + +@dataclass(frozen=True) +class JtacInfo: + """JTAC information.""" + unit_name: str + callsign: str + region: str + code: str + # TODO: Radio info? Type? + + class GroundConflictGenerator: def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance): @@ -32,6 +45,7 @@ class GroundConflictGenerator: self.player_stance = CombatStance(player_stance) self.enemy_stance = random.choice([CombatStance.AGGRESIVE, CombatStance.AGGRESIVE, CombatStance.AGGRESIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESIVE]) self.game = game + self.jtacs: List[JtacInfo] = [] def _group_point(self, point) -> Point: distance = randint( @@ -100,7 +114,7 @@ class GroundConflictGenerator: # Add JTAC if "has_jtac" in self.game.player_faction and self.game.player_faction["has_jtac"] and self.game.settings.include_jtac_if_available: n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id) - code = 1688 - len(self.game.jtacs) + code = 1688 - len(self.jtacs) utype = MQ_9_Reaper if "jtac_unit" in self.game.player_faction: @@ -115,7 +129,10 @@ class GroundConflictGenerator: jtac.points[0].tasks.append(SetInvisibleCommand(True)) jtac.points[0].tasks.append(SetImmortalCommand(True)) jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)) - self.game.jtacs.append(("Frontline " + self.conflict.from_cp.name + "/" + self.conflict.to_cp.name, code, n)) + frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}" + # Note: Will need to change if we ever add ground based JTAC. + callsign = callsign_for_support_unit(jtac) + self.jtacs.append(JtacInfo(n, callsign, frontline, str(code))) def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading): diff --git a/gen/briefinggen.py b/gen/briefinggen.py index 0b494ac5..5100ed70 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -2,13 +2,14 @@ import os from collections import defaultdict from dataclasses import dataclass import random -from typing import List, Tuple +from typing import List from game import db from dcs.mission import Mission from .aircraft import FlightData from .airfields import RunwayData from .airsupportgen import AwacsInfo, TankerInfo +from .armor import JtacInfo from .conflictgen import Conflict from .ground_forces.combat_stance import CombatStance from .radios import RadioFrequency @@ -21,14 +22,6 @@ class CommInfo: 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. diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 4bc5d64d..bca23746 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -109,9 +109,7 @@ class BriefingPage(KneeboardPage): def write(self, path: Path) -> None: writer = KneeboardPageWriter() - # TODO: Assign callsigns to flights and include that info. - # https://github.com/Khopa/dcs_liberation/issues/113 - writer.title(f"Mission Info") + writer.title(f"{self.flight.callsign} Mission Info") # TODO: Handle carriers. writer.heading("Airfield Info")