Add carrier support to kneeboards.

This commit is contained in:
Dan Albert 2020-09-01 14:10:57 -07:00
parent a9e65cc83d
commit d02a3a0d3f
7 changed files with 191 additions and 128 deletions

View File

@ -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.

View File

@ -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

View File

@ -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
)

View File

@ -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:

View File

@ -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:

View File

@ -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")

View File

@ -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