Add AirAssault and Airlift mission types with CTLD support

- Add the new airassault mission type and special flightplans for it
- Add the mission type to airbase and FOB
- Add Layout for the UH-1H
- Add mission type to capable squadrons
- Allow the auto planner to task air assault missions when preconditions are met
- Improve Airlift mission type and improve the flightplan (Stopover and Helo landing)
- Allow Slingload and spawnable crates for airlift
- Rework airsupport to a general missiondata class
- Added Carrier Information to mission data
- Allow to define CTLD specific capabilities in the unit yaml
- Allow inflight preload and fixed wing support for air assault
This commit is contained in:
RndName
2022-04-12 12:42:17 +02:00
parent de148dbb61
commit aa77cfe4b9
73 changed files with 996 additions and 212 deletions

View File

@@ -483,6 +483,20 @@ TRANSPORT_CAPABLE = [
Mi_26,
]
AIR_ASSAULT_CAPABLE = [
CH_53E,
CH_47D,
UH_60L,
SH_60B,
UH_60A,
UH_1H,
Mi_8MT,
Mi_26,
Mi_24P,
Mi_24V,
Hercules,
]
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
AEWC_CAPABLE = [
@@ -538,6 +552,8 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
return REFUELING_CAPABALE
elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE
elif task == FlightType.AIR_ASSAULT:
return AIR_ASSAULT_CAPABLE
else:
logging.error(f"Unplannable flight type: {task}")
return []

View File

@@ -139,6 +139,10 @@ class Flight(SidcDescribable):
def unit_type(self) -> AircraftType:
return self.squadron.aircraft
@property
def is_helo(self) -> bool:
return self.unit_type.dcs_unit_type.helicopter
@property
def from_cp(self) -> ControlPoint:
return self.departure

View File

@@ -0,0 +1,128 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Iterator, Type
from game.ato.flightplans.airlift import AirliftLayout
from game.ato.flightplans.standard import StandardFlightPlan
from game.theater.controlpoint import ControlPointType
from game.theater.missiontarget import MissionTarget
from game.utils import Distance, feet, meters
from .ibuilder import IBuilder
from .waypointbuilder import WaypointBuilder
if TYPE_CHECKING:
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
class Builder(IBuilder):
def build(self) -> AirAssaultLayout:
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
altitude_is_agl = self.flight.is_helo
builder = WaypointBuilder(self.flight, self.coalition)
if not self.flight.is_helo or self.flight.departure.cptype in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
ControlPointType.OFF_MAP,
]:
# Non-Helo flights or Off_Map will be preloaded
# Carrier operations load the logistics directly from the carrier
pickup = None
pickup_position = self.flight.departure.position
else:
# Create a special pickup zone for Helos from Airbase / FOB
pickup = builder.pickup(
MissionTarget(
"Pickup Zone",
self.flight.departure.position.random_point_within(1200, 600),
)
)
pickup_position = pickup.position
assault_area = builder.assault_area(self.package.target)
heading = self.package.target.position.heading_between_point(pickup_position)
drop_off_zone = MissionTarget(
"Dropoff zone",
self.package.target.position.point_from_heading(heading, 1200),
)
return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure),
nav_to_pickup=builder.nav_path(
self.flight.departure.position,
pickup_position,
altitude,
altitude_is_agl,
),
pickup=pickup,
nav_to_drop_off=builder.nav_path(
pickup_position,
drop_off_zone.position,
altitude,
altitude_is_agl,
),
drop_off=builder.drop_off(drop_off_zone),
stopover=None,
target=assault_area,
nav_to_home=builder.nav_path(
drop_off_zone.position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
@dataclass(frozen=True)
class AirAssaultLayout(AirliftLayout):
target: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_pickup
if self.pickup:
yield self.pickup
yield from self.nav_to_drop_off
yield self.drop_off
yield self.target
yield from self.nav_to_home
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
def __init__(self, flight: Flight, layout: AirAssaultLayout) -> None:
super().__init__(flight, layout)
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def tot_waypoint(self) -> FlightWaypoint | None:
return self.layout.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.tot_waypoint:
return self.tot
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
def engagement_distance(self) -> Distance:
# The radius of the WaypointZone created at the target location
return meters(2500)
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target

View File

@@ -4,6 +4,7 @@ from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget
from game.utils import feet
from .ibuilder import IBuilder
@@ -30,15 +31,48 @@ class Builder(IBuilder):
builder = WaypointBuilder(self.flight, self.coalition)
pickup = None
nav_to_pickup = []
if cargo.origin != self.flight.departure:
pickup = builder.pickup(cargo.origin)
nav_to_pickup = builder.nav_path(
self.flight.departure.position,
cargo.origin.position,
altitude,
altitude_is_agl,
stopover = None
if self.flight.is_helo:
# Create a pickupzone where the cargo will be spawned
pickup_zone = MissionTarget(
"Pickup Zone", cargo.origin.position.random_point_within(1000, 200)
)
pickup = builder.pickup(pickup_zone)
# If The cargo is at the departure controlpoint, the pickup waypoint should
# only be created for client flights
pickup.only_for_player = cargo.origin == self.flight.departure
# Create a dropoff zone where the cargo should be dropped
drop_off_zone = MissionTarget(
"Dropoff zone",
cargo.next_stop.position.random_point_within(1000, 200),
)
drop_off = builder.drop_off(drop_off_zone)
# Add an additional stopover point so that the flight can refuel
stopover = builder.stopover(cargo.next_stop)
else:
# Fixed Wing will get stopover points for pickup and dropoff
if cargo.origin != self.flight.departure:
pickup = builder.stopover(cargo.origin, "PICKUP")
drop_off = builder.stopover(cargo.next_stop, "DROP OFF")
nav_to_pickup = builder.nav_path(
self.flight.departure.position,
cargo.origin.position,
altitude,
altitude_is_agl,
)
if self.flight.client_count > 0:
# Normal Landing Waypoint
arrival = builder.land(self.flight.arrival)
else:
# The AI Needs another Stopover point to actually fly back to the original
# base. Otherwise the Cargo drop will be the new Landing Waypoint and the
# AI will end its mission there instead of flying back.
# https://forum.dcs.world/topic/211775-landing-to-refuel-and-rearm-the-landingrefuar-waypoint/
arrival = builder.stopover(self.flight.arrival, "LANDING")
return AirliftLayout(
departure=builder.takeoff(self.flight.departure),
@@ -50,14 +84,15 @@ class Builder(IBuilder):
altitude,
altitude_is_agl,
),
drop_off=builder.drop_off(cargo.next_stop),
drop_off=drop_off,
stopover=stopover,
nav_to_home=builder.nav_path(
cargo.origin.position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=builder.land(self.flight.arrival),
arrival=arrival,
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
@@ -69,15 +104,18 @@ class AirliftLayout(StandardLayout):
pickup: FlightWaypoint | None
nav_to_drop_off: list[FlightWaypoint]
drop_off: FlightWaypoint
stopover: FlightWaypoint | None
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_pickup
if self.pickup:
if self.pickup is not None:
yield self.pickup
yield from self.nav_to_drop_off
yield self.drop_off
if self.stopover is not None:
yield self.stopover
yield from self.nav_to_home
yield self.arrival
if self.divert is not None:

View File

@@ -8,6 +8,7 @@ from game.data.doctrine import Doctrine
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
from .aewc import AewcFlightPlan
from .airassault import AirAssaultFlightPlan
from .airlift import AirliftFlightPlan
from .antiship import AntiShipFlightPlan
from .bai import BaiFlightPlan
@@ -108,6 +109,7 @@ class FlightPlanBuilder:
FlightType.AEWC: AewcFlightPlan,
FlightType.TRANSPORT: AirliftFlightPlan,
FlightType.FERRY: FerryFlightPlan,
FlightType.AIR_ASSAULT: AirAssaultFlightPlan,
}
return plan_dict.get(task)

View File

@@ -54,7 +54,7 @@ class WaypointBuilder:
@property
def is_helo(self) -> bool:
return self.flight.unit_type.dcs_unit_type.helicopter
return self.flight.is_helo
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
@@ -303,6 +303,9 @@ class WaypointBuilder:
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
def assault_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"ASSAULT {target.name}", target)
@staticmethod
def _target_area(
name: str,
@@ -491,36 +494,57 @@ class WaypointBuilder:
)
@staticmethod
def pickup(control_point: ControlPoint) -> FlightWaypoint:
"""Creates a cargo pickup waypoint.
def stopover(stopover: ControlPoint, name: str = "STOPOVER") -> FlightWaypoint:
"""Creates a stopover waypoint.
Args:
control_point: Pick up location.
"""
return FlightWaypoint(
"PICKUP",
FlightWaypointType.PICKUP,
control_point.position,
name,
FlightWaypointType.STOPOVER,
stopover.position,
meters(0),
"RADIO",
description=f"Pick up cargo from {control_point}",
pretty_name="Pick up location",
description=f"Stopover at {stopover}",
pretty_name="Stopover location",
control_point=stopover,
)
@staticmethod
def drop_off(control_point: ControlPoint) -> FlightWaypoint:
def pickup(pick_up: MissionTarget) -> FlightWaypoint:
"""Creates a cargo pickup waypoint.
Args:
control_point: Pick up location.
"""
control_point = pick_up if isinstance(pick_up, ControlPoint) else None
return FlightWaypoint(
"PICKUP",
FlightWaypointType.PICKUP,
pick_up.position,
meters(0),
"RADIO",
description=f"Pick up cargo from {pick_up.name}",
pretty_name="Pick up location",
control_point=control_point,
)
@staticmethod
def drop_off(drop_off: MissionTarget) -> FlightWaypoint:
"""Creates a cargo drop-off waypoint.
Args:
control_point: Drop-off location.
"""
control_point = drop_off if isinstance(drop_off, ControlPoint) else None
return FlightWaypoint(
"DROP OFF",
FlightWaypointType.PICKUP,
control_point.position,
FlightWaypointType.DROP_OFF,
drop_off.position,
meters(0),
"RADIO",
description=f"Drop off cargo at {control_point}",
description=f"Drop off cargo at {drop_off.name}",
pretty_name="Drop off location",
control_point=control_point,
)

View File

@@ -56,6 +56,7 @@ class FlightType(Enum):
SEAD_ESCORT = "SEAD Escort"
REFUELING = "Refueling"
FERRY = "Ferry"
AIR_ASSAULT = "Air Assault"
def __str__(self) -> str:
return self.value
@@ -89,6 +90,7 @@ class FlightType(Enum):
FlightType.OCA_RUNWAY,
FlightType.OCA_AIRCRAFT,
FlightType.SEAD_ESCORT,
FlightType.AIR_ASSAULT,
}
@property
@@ -112,4 +114,5 @@ class FlightType(Enum):
FlightType.SWEEP: AirEntity.FIGHTER,
FlightType.TARCAP: AirEntity.FIGHTER,
FlightType.TRANSPORT: AirEntity.UTILITY,
FlightType.AIR_ASSAULT: AirEntity.ROTARY_WING,
}.get(self, AirEntity.UNSPECIFIED)

View File

@@ -47,3 +47,4 @@ class FlightWaypointType(IntEnum):
DROP_OFF = 27
BULLSEYE = 28
REFUEL = 29 # Should look for nearby tanker to refuel from.
STOPOVER = 30 # Stopover landing point using the LandingReFuAr waypoint type

View File

@@ -161,6 +161,7 @@ class Package:
FlightType.BAI,
FlightType.DEAD,
FlightType.TRANSPORT,
FlightType.AIR_ASSAULT,
FlightType.SEAD,
FlightType.TARCAP,
FlightType.BARCAP,

View File

@@ -139,6 +139,14 @@ class ObjectiveFinder:
"""Iterates over all active front lines in the theater."""
yield from self.game.theater.conflicts()
def air_assault_targets(self) -> Iterator[ControlPoint]:
"""Iterates over all capturable controlpoints for all active front lines"""
if not self.game.settings.plugin_option("ctld"):
# Air Assault should only be tasked with CTLD enabled
return
for front_line in self.front_lines():
yield front_line.control_point_hostile_to(self.is_player)
def vulnerable_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over friendly CPs that are vulnerable to enemy CPs.

View File

@@ -7,6 +7,7 @@ from game.commander.tasks.compound.destroyenemygroundunits import (
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
ReduceEnemyFrontLineCapacity,
)
from game.commander.tasks.primitive.airassault import PlanAirAssault
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@@ -18,6 +19,7 @@ class CaptureBase(CompoundTask[TheaterState]):
front_line: FrontLine
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [PlanAirAssault(self.enemy_cp(state))]
yield [BreakthroughAttack(self.front_line, state.context.coalition.player)]
yield [DestroyEnemyGroundUnits(self.front_line)]
if self.worth_destroying_ammo_depots(state):

View File

@@ -0,0 +1,34 @@
from __future__ import annotations
from dataclasses import dataclass
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.theater import ControlPoint
from game.ato.flighttype import FlightType
@dataclass
class PlanAirAssault(PackagePlanningTask[ControlPoint]):
def preconditions_met(self, state: TheaterState) -> bool:
if self.target not in state.air_assault_targets:
return False
if self.capture_blocked(state):
# Do not task if there are enemy garrisons blocking the capture
return False
if not self.target_area_preconditions_met(state):
# Do not task if air defense is present in the target area
return False
return super().preconditions_met(state)
def capture_blocked(self, state: TheaterState) -> bool:
garrisons = state.enemy_garrisons[self.target]
return len(garrisons.blocking_capture) > 0
def apply_effects(self, state: TheaterState) -> None:
state.air_assault_targets.remove(self.target)
def propose_flights(self) -> None:
self.propose_flight(FlightType.AIR_ASSAULT, 2)
# TODO Validate this.. / is Heli escort possible?
self.propose_flight(FlightType.TARCAP, 2)

View File

@@ -45,6 +45,7 @@ class TheaterState(WorldState["TheaterState"]):
context: PersistentContext
barcaps_needed: dict[ControlPoint, int]
active_front_lines: list[FrontLine]
air_assault_targets: list[ControlPoint]
front_line_stances: dict[FrontLine, Optional[CombatStance]]
vulnerable_front_lines: list[FrontLine]
aewc_targets: list[MissionTarget]
@@ -109,6 +110,7 @@ class TheaterState(WorldState["TheaterState"]):
context=self.context,
barcaps_needed=dict(self.barcaps_needed),
active_front_lines=list(self.active_front_lines),
air_assault_targets=list(self.air_assault_targets),
front_line_stances=dict(self.front_line_stances),
vulnerable_front_lines=list(self.vulnerable_front_lines),
aewc_targets=list(self.aewc_targets),
@@ -158,6 +160,7 @@ class TheaterState(WorldState["TheaterState"]):
cp: barcap_rounds for cp in finder.vulnerable_control_points()
},
active_front_lines=list(finder.front_lines()),
air_assault_targets=list(finder.air_assault_targets()),
front_line_stances={f: None for f in finder.front_lines()},
vulnerable_front_lines=list(finder.front_lines()),
aewc_targets=[finder.farthest_friendly_control_point()],

View File

@@ -18,6 +18,7 @@ class UnitClass(Enum):
EARLY_WARNING_RADAR = "EarlyWarningRadar"
FORTIFICATION = "Fortification"
FRIGATE = "Frigate"
HELICOPTER = "Helicopter"
HELICOPTER_CARRIER = "HelicopterCarrier"
IFV = "IFV"
INFANTRY = "Infantry"

View File

@@ -47,7 +47,7 @@ from game.utils import (
if TYPE_CHECKING:
from game.missiongenerator.aircraft.flightdata import FlightData
from game.missiongenerator.airsupport import AirSupport
from game.missiongenerator.missiondata import MissionData
from game.radio.radios import Radio, RadioFrequency, RadioRegistry
@@ -182,6 +182,14 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer]
# Logisitcs info
# cabin_size defines how many troops can be loaded. 0 means the aircraft can not
# transport any troops. Default for helos is 10, non helos will have 0.
cabin_size: int
# If the aircraft can carry crates can_carry_crates should be set to true which
# will be set to true for helos by default
can_carry_crates: bool
@property
def flyable(self) -> bool:
return self.dcs_unit_type.flyable
@@ -281,10 +289,10 @@ class AircraftType(UnitType[Type[FlyingType]]):
return freq
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
self, flight: FlightData, mission_data: MissionData
) -> None:
if self.channel_allocator is not None:
self.channel_allocator.assign_channels_for_flight(flight, air_support)
self.channel_allocator.assign_channels_for_flight(flight, mission_data)
def channel_name(self, radio_id: int, channel_id: int) -> str:
return self.channel_namer.channel_name(radio_id, channel_id)
@@ -387,6 +395,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
if units_data == "metric":
units = MetricUnits()
class_name = data.get("class")
unit_class = UnitClass.PLANE if class_name is None else UnitClass(class_name)
prop_overrides = data.get("default_overrides")
if prop_overrides is not None:
cls._set_props_overrides(prop_overrides, aircraft, data_path)
@@ -419,5 +430,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_namer=radio_config.channel_namer,
kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False),
unit_class=UnitClass.PLANE,
unit_class=unit_class,
cabin_size=data.get("cabin_size", 10 if aircraft.helicopter else 0),
can_carry_crates=data.get("can_carry_crates", aircraft.helicopter),
)

View File

@@ -62,7 +62,10 @@ class AircraftBehavior:
self.configure_runway_attack(group, flight)
elif self.task == FlightType.OCA_AIRCRAFT:
self.configure_oca_strike(group, flight)
elif self.task == FlightType.TRANSPORT:
elif self.task in [
FlightType.TRANSPORT,
FlightType.AIR_ASSAULT,
]:
self.configure_transport(group, flight)
elif self.task == FlightType.FERRY:
self.configure_ferry(group, flight)

View File

@@ -17,7 +17,7 @@ from game.ato.flighttype import FlightType
from game.ato.package import Package
from game.ato.starttype import StartType
from game.factions.faction import Faction
from game.missiongenerator.airsupport import AirSupport
from game.missiongenerator.missiondata import MissionData
from game.missiongenerator.lasercoderegistry import LaserCodeRegistry
from game.radio.radios import RadioRegistry
from game.radio.tacan import TacanRegistry
@@ -49,7 +49,7 @@ class AircraftGenerator:
tacan_registry: TacanRegistry,
laser_code_registry: LaserCodeRegistry,
unit_map: UnitMap,
air_support: AirSupport,
mission_data: MissionData,
helipads: dict[ControlPoint, StaticGroup],
) -> None:
self.mission = mission
@@ -61,7 +61,7 @@ class AircraftGenerator:
self.laser_code_registry = laser_code_registry
self.unit_map = unit_map
self.flights: List[FlightData] = []
self.air_support = air_support
self.mission_data = mission_data
self.helipads = helipads
@cached_property
@@ -174,7 +174,7 @@ class AircraftGenerator:
self.radio_registry,
self.tacan_registy,
self.laser_code_registry,
self.air_support,
self.mission_data,
dynamic_runways,
self.use_client,
).configure()

View File

@@ -12,8 +12,9 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightType
from game.callsigns import callsign_for_support_unit
from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum
from game.missiongenerator.airsupport import AirSupport, AwacsInfo, TankerInfo
from game.missiongenerator.missiondata import MissionData, AwacsInfo, TankerInfo
from game.missiongenerator.lasercoderegistry import LaserCodeRegistry
from game.missiongenerator.logisticsgenerator import LogisticsGenerator
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
from game.runways import RunwayData
@@ -40,7 +41,7 @@ class FlightGroupConfigurator:
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
laser_code_registry: LaserCodeRegistry,
air_support: AirSupport,
mission_data: MissionData,
dynamic_runways: dict[str, RunwayData],
use_client: bool,
) -> None:
@@ -52,7 +53,7 @@ class FlightGroupConfigurator:
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.laser_code_registry = laser_code_registry
self.air_support = air_support
self.mission_data = mission_data
self.dynamic_runways = dynamic_runways
self.use_client = use_client
@@ -74,6 +75,20 @@ class FlightGroupConfigurator:
self.game.theater, self.game.conditions, self.dynamic_runways
)
if self.flight.flight_type in [
FlightType.TRANSPORT,
FlightType.AIR_ASSAULT,
] and self.game.settings.plugin_option("ctld"):
transfer = None
if self.flight.flight_type == FlightType.TRANSPORT:
coalition = self.game.coalition_for(player=self.flight.blue)
transfer = coalition.transfers.transfer_for_flight(self.flight)
self.mission_data.logistics.append(
LogisticsGenerator(
self.flight, self.group, self.mission, self.game.settings, transfer
).generate_logistics()
)
mission_start_time, waypoints = WaypointGenerator(
self.flight,
self.group,
@@ -81,7 +96,7 @@ class FlightGroupConfigurator:
self.game.conditions.start_time,
self.time,
self.game.settings,
self.air_support,
self.mission_data,
).create_waypoints()
return FlightData(
@@ -130,7 +145,7 @@ class FlightGroupConfigurator:
def register_air_support(self, channel: RadioFrequency) -> None:
callsign = callsign_for_support_unit(self.group)
if isinstance(self.flight.flight_plan, AewcFlightPlan):
self.air_support.awacs.append(
self.mission_data.awacs.append(
AwacsInfo(
group_name=str(self.group.name),
callsign=callsign,
@@ -143,7 +158,7 @@ class FlightGroupConfigurator:
)
elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan):
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
self.air_support.tankers.append(
self.mission_data.tankers.append(
TankerInfo(
group_name=str(self.group.name),
callsign=callsign,

View File

@@ -1,4 +1,9 @@
from dcs.point import MovingPoint, PointAction
from dcs.point import MovingPoint
from dcs.task import Land
from game.utils import feet
from dcs.point import PointAction
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -6,9 +11,16 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class CargoStopBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
waypoint.type = "LandingReFuAr"
waypoint.action = PointAction.LandingReFuAr
waypoint.landing_refuel_rearm_time = 2 # Minutes.
if (control_point := self.waypoint.control_point) is not None:
waypoint.airdrome_id = control_point.airdrome_id_for_landing
# Create a landing task, currently only for Helos!
if self.flight.is_helo:
# Calculate a landing point with a small buffer to prevent AI from landing
# directly at the static ammo depot and exploding
landing_point = waypoint.position.random_point_within(15, 5)
# Use Land Task with 30s duration for helos
waypoint.add_task(Land(landing_point, duration=30))
else:
# Fixed wing will drop the cargo at the waypoint so we set a lower altitude
waypoint.alt = int(feet(10000).meters)
waypoint.alt_type = "BARO"
waypoint.action = PointAction.FlyOverPoint
return waypoint

View File

@@ -10,7 +10,7 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.airsupport import AirSupport
from game.missiongenerator.missiondata import MissionData
from game.theater import MissionTarget, TheaterUnit
TARGET_WAYPOINTS = (
@@ -28,7 +28,7 @@ class PydcsWaypointBuilder:
flight: Flight,
mission: Mission,
elapsed_mission_time: timedelta,
air_support: AirSupport,
mission_data: MissionData,
) -> None:
self.waypoint = waypoint
self.group = group
@@ -36,7 +36,7 @@ class PydcsWaypointBuilder:
self.flight = flight
self.mission = mission
self.elapsed_mission_time = elapsed_mission_time
self.air_support = air_support
self.mission_data = mission_data
def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint(

View File

@@ -64,7 +64,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
waypoint.add_task(Tanker())
if self.flight.unit_type.dcs_unit_type.tacan:
tanker_info = self.air_support.tankers[-1]
tanker_info = self.mission_data.tankers[-1]
tacan = tanker_info.tacan
tacan_callsign = {
"Texaco": "TEX",

View File

@@ -0,0 +1,15 @@
from dcs.point import MovingPoint, PointAction
from dcs.task import Land
from .pydcswaypointbuilder import PydcsWaypointBuilder
class StopoverBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
waypoint.type = "LandingReFuAr"
waypoint.action = PointAction.LandingReFuAr
waypoint.landing_refuel_rearm_time = 2 # Minutes.
if (control_point := self.waypoint.control_point) is not None:
waypoint.airdrome_id = control_point.airdrome_id_for_landing
return waypoint

View File

@@ -16,7 +16,8 @@ from game.ato import Flight, FlightWaypoint
from game.ato.flightstate import InFlight, WaitingForStart
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType
from game.missiongenerator.airsupport import AirSupport
from game.missiongenerator.aircraft.waypoints.stopover import StopoverBuilder
from game.missiongenerator.missiondata import MissionData
from game.settings import Settings
from game.utils import pairwise
from .baiingress import BaiIngressBuilder
@@ -48,7 +49,7 @@ class WaypointGenerator:
turn_start_time: datetime,
time: datetime,
settings: Settings,
air_support: AirSupport,
mission_data: MissionData,
) -> None:
self.flight = flight
self.group = group
@@ -56,7 +57,7 @@ class WaypointGenerator:
self.elapsed_mission_time = time - turn_start_time
self.time = time
self.settings = settings
self.air_support = air_support
self.mission_data = mission_data
def create_waypoints(self) -> tuple[timedelta, list[FlightWaypoint]]:
for waypoint in self.flight.points:
@@ -134,6 +135,7 @@ class WaypointGenerator:
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
FlightWaypointType.PICKUP: CargoStopBuilder,
FlightWaypointType.REFUEL: RefuelPointBuilder,
FlightWaypointType.STOPOVER: StopoverBuilder,
}
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(
@@ -142,7 +144,7 @@ class WaypointGenerator:
self.flight,
self.mission,
self.elapsed_mission_time,
self.air_support,
self.mission_data,
)
def _estimate_min_fuel_for(self, waypoints: list[FlightWaypoint]) -> None:

View File

@@ -1,56 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import timedelta
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from game.radio.radios import RadioFrequency
from game.radio.tacan import TacanChannel
@dataclass
class AwacsInfo:
"""AWACS information for the kneeboard."""
group_name: str
callsign: str
freq: RadioFrequency
depature_location: Optional[str]
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@dataclass
class TankerInfo:
"""Tanker information for the kneeboard."""
group_name: str
callsign: str
variant: str
freq: RadioFrequency
tacan: TacanChannel
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@dataclass(frozen=True)
class JtacInfo:
"""JTAC information."""
group_name: str
unit_name: str
callsign: str
region: str
code: str
blue: bool
freq: RadioFrequency
@dataclass
class AirSupport:
awacs: list[AwacsInfo] = field(default_factory=list)
tankers: list[TankerInfo] = field(default_factory=list)
jtacs: list[JtacInfo] = field(default_factory=list)

View File

@@ -21,7 +21,7 @@ from game.radio.radios import RadioRegistry
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
from game.utils import Heading
from game.ato.ai_flight_planner_db import AEWC_CAPABLE
from .airsupport import AirSupport, AwacsInfo, TankerInfo
from .missiondata import MissionData, AwacsInfo, TankerInfo
from .frontlineconflictdescription import FrontLineConflictDescription
if TYPE_CHECKING:
@@ -43,14 +43,14 @@ class AirSupportGenerator:
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
air_support: AirSupport,
mission_data: MissionData,
) -> None:
self.mission = mission
self.conflict = conflict
self.game = game
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.air_support = air_support
self.mission_data = mission_data
@classmethod
def support_tasks(cls) -> List[Type[MainTask]]:
@@ -148,13 +148,13 @@ class AirSupportGenerator:
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(
self.mission_data.tankers.append(
TankerInfo(
str(tanker_group.name),
callsign,
tanker_unit_type.name,
freq,
tacan,
group_name=str(tanker_group.name),
callsign=callsign,
variant=tanker_unit_type.name,
freq=freq,
tacan=tacan,
start_time=None,
end_time=None,
blue=True,
@@ -197,7 +197,7 @@ class AirSupportGenerator:
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append(
self.mission_data.awacs.append(
AwacsInfo(
group_name=str(awacs_flight.name),
callsign=callsign_for_support_unit(awacs_flight),

View File

@@ -43,7 +43,7 @@ from game.radio.radios import RadioRegistry
from game.theater.controlpoint import ControlPoint
from game.unitmap import UnitMap
from game.utils import Heading
from .airsupport import AirSupport, JtacInfo
from .missiondata import MissionData, JtacInfo
from .frontlineconflictdescription import FrontLineConflictDescription
from .lasercoderegistry import LaserCodeRegistry
@@ -80,7 +80,7 @@ class FlotGenerator:
enemy_stance: CombatStance,
unit_map: UnitMap,
radio_registry: RadioRegistry,
air_support: AirSupport,
mission_data: MissionData,
laser_code_registry: LaserCodeRegistry,
) -> None:
self.mission = mission
@@ -92,7 +92,7 @@ class FlotGenerator:
self.game = game
self.unit_map = unit_map
self.radio_registry = radio_registry
self.air_support = air_support
self.mission_data = mission_data
self.laser_code_registry = laser_code_registry
def generate(self) -> None:
@@ -166,7 +166,7 @@ class FlotGenerator:
)
jtac.points[0].tasks.append(
FAC(
callsign=len(self.air_support.jtacs) + 1,
callsign=len(self.mission_data.jtacs) + 1,
frequency=int(freq.mhz),
modulation=freq.modulation,
)
@@ -181,13 +181,13 @@ class FlotGenerator:
)
# Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac)
self.air_support.jtacs.append(
self.mission_data.jtacs.append(
JtacInfo(
jtac.name,
jtac.name,
callsign,
frontline,
str(code),
group_name=jtac.name,
unit_name=jtac.units[0].name,
callsign=callsign,
region=frontline,
code=str(code),
blue=True,
freq=freq,
)

View File

@@ -0,0 +1,104 @@
from typing import Any, Optional
from dcs import Mission
from dcs.unitgroup import FlyingGroup
from dcs.statics import Fortification
from game.ato import Flight
from game.ato.flightplans.airassault import AirAssaultFlightPlan
from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.missiondata import CargoInfo, LogisticsInfo
from game.settings.settings import Settings
from game.transfers import TransferOrder
ZONE_RADIUS = 300
CRATE_ZONE_RADIUS = 50
class LogisticsGenerator:
def __init__(
self,
flight: Flight,
group: FlyingGroup[Any],
mission: Mission,
settings: Settings,
transfer: Optional[TransferOrder] = None,
) -> None:
self.flight = flight
self.group = group
self.transfer = transfer
self.mission = mission
self.settings = settings
def generate_logistics(self) -> LogisticsInfo:
# Add Logisitcs info for the flight
logistics_info = LogisticsInfo(
pilot_names=[u.name for u in self.group.units],
transport=self.flight.squadron.aircraft,
blue=self.flight.blue,
preload=self.flight.state.in_flight,
)
if isinstance(self.flight.flight_plan, AirAssaultFlightPlan):
# Preload fixed wing as they do not have a pickup zone
logistics_info.preload = logistics_info.preload or not self.flight.is_helo
# Create the Waypoint Zone used by CTLD
target_zone = f"{self.group.name}TARGET_ZONE"
self.mission.triggers.add_triggerzone(
self.flight.flight_plan.layout.target.position,
self.flight.flight_plan.engagement_distance.meters,
False,
target_zone,
)
logistics_info.target_zone = target_zone
pickup_point = None
for waypoint in self.flight.points:
if (
waypoint.waypoint_type
not in [
FlightWaypointType.PICKUP,
FlightWaypointType.DROP_OFF,
]
or waypoint.only_for_player
and not self.flight.client_count
):
continue
# Create Pickup and DropOff zone
zone_name = f"{self.group.name}{waypoint.waypoint_type.name}"
self.mission.triggers.add_triggerzone(
waypoint.position, ZONE_RADIUS, False, zone_name
)
if waypoint.waypoint_type == FlightWaypointType.PICKUP:
pickup_point = waypoint.position
logistics_info.pickup_zone = zone_name
else:
logistics_info.drop_off_zone = zone_name
if self.transfer and self.flight.client_count > 0 and pickup_point is not None:
# Add spawnable crates for client airlifts
crate_location = pickup_point.random_point_within(
ZONE_RADIUS - CRATE_ZONE_RADIUS, CRATE_ZONE_RADIUS
)
crate_zone = f"{self.group.name}crate_spawn"
self.mission.triggers.add_triggerzone(
crate_location, CRATE_ZONE_RADIUS, False, crate_zone
)
logistics_info.cargo = [
CargoInfo(cargo_unit_type.dcs_id, crate_zone, amount)
for cargo_unit_type, amount in self.transfer.units.items()
]
if pickup_point is not None and self.settings.plugin_option(
"ctld.logisticunit"
):
# Spawn logisticsunit at pickup zones
country = self.mission.country(self.flight.country)
logistic_unit = self.mission.static_group(
country,
f"{self.group.name}logistic",
Fortification.FARP_Ammo_Dump_Coating,
pickup_point,
)
logistics_info.logistic_unit = logistic_unit.units[0].name
return logistics_info

View File

@@ -12,13 +12,13 @@ from dcs.translation import String
from dcs.triggers import TriggerStart
from game.ato import FlightType
from game.dcs.aircrafttype import AircraftType
from game.plugins import LuaPluginManager
from game.theater import TheaterGroundObject
from game.theater.iadsnetwork.iadsrole import IadsRole
from game.utils import escape_string_for_lua
from .aircraft.flightdata import FlightData
from .airsupport import AirSupport
from .missiondata import MissionData
if TYPE_CHECKING:
from game import Game
@@ -29,13 +29,11 @@ class LuaGenerator:
self,
game: Game,
mission: Mission,
air_support: AirSupport,
flights: list[FlightData],
mission_data: MissionData,
) -> None:
self.game = game
self.mission = mission
self.air_support = air_support
self.flights = flights
self.mission_data = mission_data
self.plugin_scripts: list[str] = []
def generate(self) -> None:
@@ -49,10 +47,20 @@ class LuaGenerator:
install_path.set_value(os.path.abspath("."))
lua_data.add_item("Airbases")
lua_data.add_item("Carriers")
carriers_object = lua_data.add_item("Carriers")
for carrier in self.mission_data.carriers:
carrier_item = carriers_object.add_item()
carrier_item.add_key_value("dcsGroupName", carrier.group_name)
carrier_item.add_key_value("unit_name", carrier.unit_name)
carrier_item.add_key_value("callsign", carrier.callsign)
carrier_item.add_key_value("radio", str(carrier.freq.mhz))
carrier_item.add_key_value(
"tacan", str(carrier.tacan.number) + carrier.tacan.band.name
)
tankers_object = lua_data.add_item("Tankers")
for tanker in self.air_support.tankers:
for tanker in self.mission_data.tankers:
tanker_item = tankers_object.add_item()
tanker_item.add_key_value("dcsGroupName", tanker.group_name)
tanker_item.add_key_value("callsign", tanker.callsign)
@@ -63,14 +71,14 @@ class LuaGenerator:
)
awacs_object = lua_data.add_item("AWACs")
for awacs in self.air_support.awacs:
for awacs in self.mission_data.awacs:
awacs_item = awacs_object.add_item()
awacs_item.add_key_value("dcsGroupName", awacs.group_name)
awacs_item.add_key_value("callsign", awacs.callsign)
awacs_item.add_key_value("radio", str(awacs.freq.mhz))
jtacs_object = lua_data.add_item("JTACs")
for jtac in self.air_support.jtacs:
for jtac in self.mission_data.jtacs:
jtac_item = jtacs_object.add_item()
jtac_item.add_key_value("dcsGroupName", jtac.group_name)
jtac_item.add_key_value("callsign", jtac.callsign)
@@ -81,16 +89,55 @@ class LuaGenerator:
jtac_item.add_key_value("modulation", jtac.freq.modulation.name)
logistics_object = lua_data.add_item("Logistics")
for logistic_info in self.air_support.logistics.values():
logistics_item = logistics_object.add_item()
logistics_flights = logistics_object.add_item("flights")
crates_object = logistics_object.add_item("crates")
spawnable_crates: dict[str, str] = {}
transports: list[AircraftType] = []
for logistic_info in self.mission_data.logistics:
if logistic_info.transport not in transports:
transports.append(logistic_info.transport)
coalition_color = "blue" if logistic_info.blue else "red"
logistics_item = logistics_flights.add_item()
logistics_item.add_data_array("pilot_names", logistic_info.pilot_names)
logistics_item.add_key_value("pickup_zone", logistic_info.pickup_zone)
logistics_item.add_key_value("drop_off_zone", logistic_info.drop_off_zone)
logistics_item.add_key_value("target_zone", logistic_info.target_zone)
logistics_item.add_key_value("side", str(2 if logistic_info.blue else 1))
logistics_item.add_key_value("logistic_unit", logistic_info.logistic_unit)
logistics_item.add_key_value(
"aircraft_type", logistic_info.transport.dcs_id
)
logistics_item.add_key_value(
"preload", "true" if logistic_info.preload else "false"
)
for cargo in logistic_info.cargo:
if cargo.unit_type not in spawnable_crates:
spawnable_crates[cargo.unit_type] = str(200 + len(spawnable_crates))
crate_weight = spawnable_crates[cargo.unit_type]
for i in range(cargo.amount):
cargo_item = crates_object.add_item()
cargo_item.add_key_value("weight", crate_weight)
cargo_item.add_key_value("coalition", coalition_color)
cargo_item.add_key_value("zone", cargo.spawn_zone)
transport_object = logistics_object.add_item("transports")
for transport in transports:
transport_item = transport_object.add_item()
transport_item.add_key_value("aircraft_type", transport.dcs_id)
transport_item.add_key_value("cabin_size", str(transport.cabin_size))
transport_item.add_key_value(
"troops", "true" if transport.cabin_size > 0 else "false"
)
transport_item.add_key_value(
"crates", "true" if transport.can_carry_crates else "false"
)
spawnable_crates_object = logistics_object.add_item("spawnable_crates")
for unit, weight in spawnable_crates.items():
crate_item = spawnable_crates_object.add_item()
crate_item.add_key_value("unit", unit)
crate_item.add_key_value("weight", weight)
target_points = lua_data.add_item("TargetPoints")
for flight in self.flights:
for flight in self.mission_data.flights:
if flight.friendly and flight.flight_type in [
FlightType.ANTISHIP,
FlightType.DEAD,
@@ -225,6 +272,9 @@ class LuaItem(ABC):
def set_value(self, value: str) -> None:
self.value = LuaValue(None, value)
def set_data_array(self, values: list[str]) -> None:
self.value = LuaValue(None, values)
def add_data_array(self, key: str, values: list[str]) -> None:
self._add_value(LuaValue(key, values))

View File

@@ -0,0 +1,96 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import timedelta
from typing import Optional, TYPE_CHECKING
from game.dcs.aircrafttype import AircraftType
from game.missiongenerator.aircraft.flightdata import FlightData
from game.runways import RunwayData
if TYPE_CHECKING:
from game.radio.radios import RadioFrequency
from game.radio.tacan import TacanChannel
@dataclass
class GroupInfo:
group_name: str
callsign: str
freq: RadioFrequency
blue: bool
@dataclass
class UnitInfo(GroupInfo):
unit_name: str
@dataclass
class AwacsInfo(GroupInfo):
"""AWACS information for the kneeboard."""
depature_location: Optional[str]
start_time: Optional[timedelta]
end_time: Optional[timedelta]
@dataclass
class TankerInfo(GroupInfo):
"""Tanker information for the kneeboard."""
variant: str
tacan: TacanChannel
start_time: Optional[timedelta]
end_time: Optional[timedelta]
@dataclass
class CarrierInfo(UnitInfo):
"""Carrier information."""
tacan: TacanChannel
@dataclass
class JtacInfo(UnitInfo):
"""JTAC information."""
region: str
code: str
@dataclass
class CargoInfo:
"""Cargo information."""
unit_type: str = field(default_factory=str)
spawn_zone: str = field(default_factory=str)
amount: int = field(default=1)
@dataclass
class LogisticsInfo:
"""Logistics information."""
pilot_names: list[str]
transport: AircraftType
blue: bool
logistic_unit: str = field(default_factory=str)
pickup_zone: str = field(default_factory=str)
drop_off_zone: str = field(default_factory=str)
target_zone: str = field(default_factory=str)
cargo: list[CargoInfo] = field(default_factory=list)
preload: bool = field(default=False)
@dataclass
class MissionData:
awacs: list[AwacsInfo] = field(default_factory=list)
runways: list[RunwayData] = field(default_factory=list)
carriers: list[CarrierInfo] = field(default_factory=list)
flights: list[FlightData] = field(default_factory=list)
tankers: list[TankerInfo] = field(default_factory=list)
jtacs: list[JtacInfo] = field(default_factory=list)
logistics: list[LogisticsInfo] = field(default_factory=list)

View File

@@ -22,7 +22,7 @@ from game.theater import Airfield, FrontLine
from game.theater.bullseye import Bullseye
from game.unitmap import UnitMap
from .aircraft.flightdata import FlightData
from .airsupport import AirSupport
from .missiondata import MissionData
from .airsupportgenerator import AirSupportGenerator
from .beacons import load_beacons_for_terrain
from .briefinggenerator import BriefingGenerator, MissionInfoGenerator
@@ -61,7 +61,7 @@ class MissionGenerator:
self.mission = Mission(game.theater.terrain)
self.unit_map = UnitMap()
self.air_support = AirSupport()
self.mission_data = MissionData()
self.laser_code_registry = LaserCodeRegistry()
self.radio_registry = RadioRegistry()
@@ -92,6 +92,7 @@ class MissionGenerator:
self.radio_registry,
self.tacan_registry,
self.unit_map,
self.mission_data,
)
tgo_generator.generate()
@@ -103,17 +104,17 @@ class MissionGenerator:
# Generate ground conflicts first so the JTACs get the first laser code (1688)
# rather than the first player flight with a TGP.
self.generate_ground_conflicts()
air_support, flights = self.generate_air_units(tgo_generator)
self.generate_air_units(tgo_generator)
TriggerGenerator(self.mission, self.game).generate()
ForcedOptionsGenerator(self.mission, self.game).generate()
VisualsGenerator(self.mission, self.game).generate()
LuaGenerator(self.game, self.mission, air_support, flights).generate()
LuaGenerator(self.game, self.mission, self.mission_data).generate()
DrawingsGenerator(self.mission, self.game).generate()
self.setup_combined_arms()
self.notify_info_generators(tgo_generator, air_support, flights)
self.notify_info_generators()
# TODO: Shouldn't this be first?
namegen.reset_numbers()
@@ -217,14 +218,12 @@ class MissionGenerator:
enemy_cp.stances[player_cp.id],
self.unit_map,
self.radio_registry,
self.air_support,
self.mission_data,
self.laser_code_registry,
)
ground_conflict_gen.generate()
def generate_air_units(
self, tgo_generator: TgoGenerator
) -> tuple[AirSupport, list[FlightData]]:
def generate_air_units(self, tgo_generator: TgoGenerator) -> None:
"""Generate the air units for the Operation"""
# Air Support (Tanker & Awacs)
@@ -234,7 +233,7 @@ class MissionGenerator:
self.game,
self.radio_registry,
self.tacan_registry,
self.air_support,
self.mission_data,
)
air_support_generator.generate()
@@ -248,7 +247,7 @@ class MissionGenerator:
self.tacan_registry,
self.laser_code_registry,
self.unit_map,
air_support=air_support_generator.air_support,
mission_data=air_support_generator.mission_data,
helipads=tgo_generator.helipads,
)
@@ -273,10 +272,10 @@ class MissionGenerator:
if not flight.client_units:
continue
flight.aircraft_type.assign_channels_for_flight(
flight, air_support_generator.air_support
flight, air_support_generator.mission_data
)
return air_support_generator.air_support, aircraft_generator.flights
self.mission_data.flights = aircraft_generator.flights
def generate_destroyed_units(self) -> None:
"""Add destroyed units to the Mission"""
@@ -325,33 +324,30 @@ class MissionGenerator:
def notify_info_generators(
self,
tgo_generator: TgoGenerator,
air_support: AirSupport,
flights: list[FlightData],
) -> None:
"""Generates subscribed MissionInfoGenerator objects."""
mission_data = self.mission_data
gens: list[MissionInfoGenerator] = [
KneeboardGenerator(self.mission, self.game),
BriefingGenerator(self.mission, self.game),
]
for gen in gens:
for dynamic_runway in tgo_generator.runways.values():
for dynamic_runway in mission_data.runways:
gen.add_dynamic_runway(dynamic_runway)
for tanker in air_support.tankers:
for tanker in mission_data.tankers:
if tanker.blue:
gen.add_tanker(tanker)
for aewc in air_support.awacs:
for aewc in mission_data.awacs:
if aewc.blue:
gen.add_awacs(aewc)
for jtac in air_support.jtacs:
for jtac in mission_data.jtacs:
if jtac.blue:
gen.add_jtac(jtac)
for flight in flights:
for flight in mission_data.flights:
gen.add_flight(flight)
gen.generate()

View File

@@ -41,6 +41,7 @@ from dcs.unit import Unit, InvisibleFARP
from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import ShipType, VehicleType
from dcs.vehicles import vehicle_map
from game.missiongenerator.missiondata import CarrierInfo, MissionData
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
@@ -351,6 +352,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
icls_alloc: Iterator[int],
runways: Dict[str, RunwayData],
unit_map: UnitMap,
mission_data: MissionData,
) -> None:
super().__init__(ground_object, country, game, mission, unit_map)
self.ground_object = ground_object
@@ -359,6 +361,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
self.tacan_registry = tacan_registry
self.icls_alloc = icls_alloc
self.runways = runways
self.mission_data = mission_data
def generate(self) -> None:
@@ -400,6 +403,16 @@ class GenericCarrierGenerator(GroundObjectGenerator):
self.add_runway_data(
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
)
self.mission_data.carriers.append(
CarrierInfo(
group_name=ship_group.name,
unit_name=ship_group.units[0].name,
callsign=tacan_callsign,
freq=atc,
tacan=tacan,
blue=self.control_point.captured,
)
)
@property
def carrier_type(self) -> Optional[Type[ShipType]]:
@@ -604,6 +617,7 @@ class TgoGenerator:
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
unit_map: UnitMap,
mission_data: MissionData,
) -> None:
self.m = mission
self.game = game
@@ -613,6 +627,7 @@ class TgoGenerator:
self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {}
self.helipads: dict[ControlPoint, StaticGroup] = {}
self.mission_data = mission_data
def generate(self) -> None:
for cp in self.game.theater.controlpoints:
@@ -640,6 +655,7 @@ class TgoGenerator:
self.icls_alloc,
self.runways,
self.unit_map,
self.mission_data,
)
elif isinstance(ground_object, LhaGroundObject):
generator = LhaGenerator(
@@ -653,6 +669,7 @@ class TgoGenerator:
self.icls_alloc,
self.runways,
self.unit_map,
self.mission_data,
)
elif isinstance(ground_object, MissileSiteGroundObject):
generator = MissileSiteGenerator(
@@ -663,3 +680,4 @@ class TgoGenerator:
ground_object, country, self.game, self.m, self.unit_map
)
generator.generate()
self.mission_data.runways = list(self.runways.values())

View File

@@ -5,14 +5,14 @@ from typing import Optional, Any, TYPE_CHECKING
if TYPE_CHECKING:
from game.missiongenerator.aircraft.flightdata import FlightData
from game.missiongenerator.airsupport import AirSupport
from game.missiongenerator.missiondata import MissionData
class RadioChannelAllocator:
"""Base class for radio channel allocators."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
self, flight: FlightData, mission_data: MissionData
) -> None:
"""Assigns mission frequencies to preset channels for the flight."""
raise NotImplementedError
@@ -44,7 +44,7 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
intra_flight_radio_index: Optional[int]
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
self, flight: FlightData, mission_data: MissionData
) -> None:
if self.intra_flight_radio_index is not None:
flight.assign_channel(
@@ -70,10 +70,10 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
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 air_support.awacs:
for awacs in mission_data.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
for jtac in air_support.jtacs:
for jtac in mission_data.jtacs:
flight.assign_channel(radio_id, next(channel_alloc), jtac.freq)
if flight.arrival != flight.departure and flight.arrival.atc is not None:
@@ -81,7 +81,7 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
try:
# TODO: Skip incompatible tankers.
for tanker in air_support.tankers:
for tanker in mission_data.tankers:
flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
if flight.divert is not None and flight.divert.atc is not None:
@@ -108,7 +108,7 @@ class NoOpChannelAllocator(RadioChannelAllocator):
"""Channel allocator for aircraft that don't support preset channels."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
self, flight: FlightData, mission_data: MissionData
) -> None:
pass
@@ -122,7 +122,7 @@ class FarmerRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the MiG-19P."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
self, flight: FlightData, mission_data: MissionData
) -> None:
# The Farmer only has 6 preset channels. It also only has a VHF radio,
# and currently our ATC data and AWACS are only in the UHF band.
@@ -141,7 +141,7 @@ class ViggenRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the AJS37."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
self, flight: FlightData, mission_data: MissionData
) -> None:
# The Viggen's preset channels are handled differently from other
# aircraft. Since 2.7.9 the group channels will not be generated automatically
@@ -161,10 +161,10 @@ class ViggenRadioChannelAllocator(RadioChannelAllocator):
radio_id, next(channel_alloc), flight.intra_flight_channel
)
for awacs in air_support.awacs:
for awacs in mission_data.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
for jtac in air_support.jtacs:
for jtac in mission_data.jtacs:
flight.assign_channel(radio_id, next(channel_alloc), jtac.freq)
if flight.departure.atc is not None:
@@ -184,7 +184,7 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
self, flight: FlightData, mission_data: MissionData
) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)

View File

@@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends
from shapely.geometry import LineString, Point as ShapelyPoint
from game import Game
from game.ato.flightplans.airassault import AirAssaultFlightPlan
from game.ato.flightplans.cas import CasFlightPlan
from game.ato.flightplans.patrolling import PatrollingFlightPlan
from game.server import GameContext
@@ -39,19 +40,23 @@ def commit_boundary(
flight_id: UUID, game: Game = Depends(GameContext.require)
) -> LeafletPoly:
flight = game.db.flights.get(flight_id)
if not isinstance(flight.flight_plan, PatrollingFlightPlan):
return []
start = flight.flight_plan.layout.patrol_start
end = flight.flight_plan.layout.patrol_end
if isinstance(flight.flight_plan, CasFlightPlan):
if isinstance(flight.flight_plan, CasFlightPlan) or isinstance(
flight.flight_plan, AirAssaultFlightPlan
):
# Special Commit boundary for CAS and AirAssault
center = flight.flight_plan.layout.target.position
commit_center = ShapelyPoint(center.x, center.y)
else:
elif isinstance(flight.flight_plan, PatrollingFlightPlan):
# Commit boundary for standard patrolling flight plan
start = flight.flight_plan.layout.patrol_start
end = flight.flight_plan.layout.patrol_end
commit_center = LineString(
[
ShapelyPoint(start.x, start.y),
ShapelyPoint(end.x, end.y),
]
)
else:
return []
bubble = commit_center.buffer(flight.flight_plan.engagement_distance.meters)
return ShapelyUtil.poly_to_leaflet(bubble, game.theater)

View File

@@ -1078,6 +1078,7 @@ class Airfield(ControlPoint):
yield from [
FlightType.OCA_AIRCRAFT,
FlightType.OCA_RUNWAY,
FlightType.AIR_ASSAULT,
]
yield from super().mission_types(for_player)
@@ -1394,6 +1395,7 @@ class Fob(ControlPoint):
if not self.is_friendly(for_player):
yield FlightType.STRIKE
yield FlightType.AIR_ASSAULT
yield from super().mission_types(for_player)

View File

@@ -98,6 +98,8 @@ class TransferOrder:
transport: Optional[Transport] = field(default=None)
request_airflift: bool = field(default=False)
def __str__(self) -> str:
"""Returns the text that should be displayed for the transfer."""
count = self.size
@@ -317,6 +319,7 @@ class AirliftPlanner:
):
self.create_airlift_flight(squadron)
if self.package.flights:
self.package.set_tot_asap()
self.game.ato_for(self.for_player).add_package(self.package)
def create_airlift_flight(self, squadron: Squadron) -> int:
@@ -585,15 +588,18 @@ class PendingTransfers:
network = self.network_for(transfer.position)
path = network.shortest_path_between(transfer.position, transfer.destination)
next_stop = path[0]
if network.link_type(transfer.position, next_stop) == TransitConnection.Road:
self.convoys.add(transfer, next_stop)
elif (
network.link_type(transfer.position, next_stop)
== TransitConnection.Shipping
):
self.cargo_ships.add(transfer, next_stop)
else:
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift()
if not transfer.request_airflift:
if (
network.link_type(transfer.position, next_stop)
== TransitConnection.Road
):
return self.convoys.add(transfer, next_stop)
elif (
network.link_type(transfer.position, next_stop)
== TransitConnection.Shipping
):
return self.cargo_ships.add(transfer, next_stop)
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift()
def new_transfer(self, transfer: TransferOrder) -> None:
transfer.origin.base.commit_losses(transfer.units)
@@ -774,3 +780,13 @@ class PendingTransfers:
self.game.coalition_for(self.player).add_procurement_request(
AircraftProcurementRequest(control_point, FlightType.TRANSPORT, gap)
)
def transfer_for_flight(self, flight: Flight) -> Optional[TransferOrder]:
for transfer in self.pending_transfers:
if transfer.transport is None or not isinstance(
transfer.transport, Airlift
):
continue
if transfer.transport.flight == flight:
return transfer
return None