mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Create a WaypointAction class that defines the actions taken at a waypoint. These will often map one-to-one with DCS waypoint actions but can also be higher level and generate multiple actions. Once everything has migrated all waypoint-type-specific behaviors of PydcsWaypointBuilder will be gone, and it'll be easier to keep the sim behaviors in sync with the mission generator behaviors. For now only hold has been migrated. This is actually probably the most complicated action we have (starting with this may have been a mistake, but it did find all the rough edges quickly) since it affects waypoint timings and flight position during simulation. That part isn't handled as neatly as I'd like because the FlightState still has to special case LOITER points to avoid simulating the wrong waypoint position. At some point we should probably start tracking real positions in FlightState, and when we do that will be solved.
299 lines
13 KiB
Python
299 lines
13 KiB
Python
import itertools
|
|
import random
|
|
from collections.abc import Iterator
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
from dcs import Mission
|
|
from dcs.action import AITaskPush, ActivateGroup
|
|
from dcs.condition import CoalitionHasAirdrome, TimeAfter
|
|
from dcs.planes import AJS37
|
|
from dcs.task import StartCommand
|
|
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
|
from dcs.unitgroup import FlyingGroup
|
|
|
|
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.aircraft.waypoints.cargostop import CargoStopBuilder
|
|
from game.missiongenerator.aircraft.waypoints.recoverytanker import (
|
|
RecoveryTankerBuilder,
|
|
)
|
|
from game.missiongenerator.missiondata import MissionData
|
|
from game.settings import Settings
|
|
from game.unitmap import UnitMap
|
|
from game.utils import pairwise
|
|
from .antishipingress import AntiShipIngressBuilder
|
|
from .baiingress import BaiIngressBuilder
|
|
from .casingress import CasIngressBuilder
|
|
from .deadingress import DeadIngressBuilder
|
|
from .default import DefaultWaypointBuilder
|
|
from .joinpoint import JoinPointBuilder
|
|
from .landingpoint import LandingPointBuilder
|
|
from .landingzone import LandingZoneBuilder
|
|
from .ocaaircraftingress import OcaAircraftIngressBuilder
|
|
from .ocarunwayingress import OcaRunwayIngressBuilder
|
|
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
|
|
from .racetrack import RaceTrackBuilder
|
|
from .racetrackend import RaceTrackEndBuilder
|
|
from .refuel import RefuelPointBuilder
|
|
from .seadingress import SeadIngressBuilder
|
|
from .splitpoint import SplitPointBuilder
|
|
from .strikeingress import StrikeIngressBuilder
|
|
from .sweepingress import SweepIngressBuilder
|
|
from .target import TargetBuilder
|
|
|
|
|
|
class WaypointGenerator:
|
|
def __init__(
|
|
self,
|
|
flight: Flight,
|
|
group: FlyingGroup[Any],
|
|
mission: Mission,
|
|
time: datetime,
|
|
settings: Settings,
|
|
mission_data: MissionData,
|
|
unit_map: UnitMap,
|
|
) -> None:
|
|
self.flight = flight
|
|
self.group = group
|
|
self.mission = mission
|
|
self.time = time
|
|
self.settings = settings
|
|
self.mission_data = mission_data
|
|
self.unit_map = unit_map
|
|
|
|
def create_waypoints(self) -> tuple[timedelta, list[FlightWaypoint]]:
|
|
for waypoint in self.flight.points:
|
|
waypoint.tot = None
|
|
|
|
waypoints = self.flight.flight_plan.waypoints
|
|
mission_start_time = self.set_takeoff_time(waypoints[0])
|
|
|
|
filtered_points: list[FlightWaypoint] = []
|
|
for point in self.flight.points:
|
|
if point.only_for_player and not self.flight.client_count:
|
|
continue
|
|
if isinstance(self.flight.state, InFlight):
|
|
if point == self.flight.state.current_waypoint:
|
|
# We don't need to build this waypoint because pydcs did that for
|
|
# us, but we do need to configure the tasks for it so that mid-
|
|
# mission aircraft starting at a waypoint with tasks behave
|
|
# correctly.
|
|
self.builder_for_waypoint(point, 1).add_tasks(self.group.points[0])
|
|
if not self.flight.state.has_passed_waypoint(point):
|
|
filtered_points.append(point)
|
|
else:
|
|
filtered_points.append(point)
|
|
# Only add 1 target waypoint for Viggens. This only affects player flights, the
|
|
# Viggen can't have more than 9 waypoints which leaves us with two target point
|
|
# under the current flight plans.
|
|
# TODO: Make this smarter. It currently targets a random unit in the group.
|
|
# This could be updated to make it pick the "best" two targets in the group.
|
|
if self.flight.unit_type.dcs_unit_type is AJS37 and self.flight.client_count:
|
|
viggen_target_points = [
|
|
(idx, point)
|
|
for idx, point in enumerate(filtered_points)
|
|
if point.waypoint_type in TARGET_WAYPOINTS
|
|
]
|
|
if viggen_target_points:
|
|
keep_target = viggen_target_points[
|
|
random.randint(0, len(viggen_target_points) - 1)
|
|
]
|
|
filtered_points = [
|
|
point
|
|
for idx, point in enumerate(filtered_points)
|
|
if (
|
|
point.waypoint_type not in TARGET_WAYPOINTS
|
|
or idx == keep_target[0]
|
|
)
|
|
]
|
|
|
|
for idx, point in enumerate(filtered_points):
|
|
# We add 2 to idx to get the generated waypoint index as
|
|
# 1) pydcs seems to decrement the index by 1 and
|
|
# 2) DCS starts the first waypoint at index 1 as 0 is the starting position
|
|
self.builder_for_waypoint(point, idx + 2).build()
|
|
|
|
# Set here rather than when the FlightData is created so they waypoints
|
|
# have their TOTs and fuel minimums set. Once we're more confident in our fuel
|
|
# estimation ability the minimum fuel amounts will be calculated during flight
|
|
# plan construction, but for now it's only used by the kneeboard so is generated
|
|
# late.
|
|
self._estimate_min_fuel_for(waypoints)
|
|
return mission_start_time, waypoints
|
|
|
|
def builder_for_waypoint(
|
|
self, waypoint: FlightWaypoint, generated_waypoint_index: int
|
|
) -> PydcsWaypointBuilder:
|
|
builders = {
|
|
FlightWaypointType.CARGO_STOP: CargoStopBuilder,
|
|
FlightWaypointType.DROPOFF_ZONE: LandingZoneBuilder,
|
|
FlightWaypointType.INGRESS_ANTI_SHIP: AntiShipIngressBuilder,
|
|
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
|
|
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
|
|
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
|
|
FlightWaypointType.INGRESS_OCA_AIRCRAFT: OcaAircraftIngressBuilder,
|
|
FlightWaypointType.INGRESS_OCA_RUNWAY: OcaRunwayIngressBuilder,
|
|
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
|
|
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
|
|
FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder,
|
|
FlightWaypointType.JOIN: JoinPointBuilder,
|
|
FlightWaypointType.LANDING_POINT: LandingPointBuilder,
|
|
FlightWaypointType.PATROL: RaceTrackEndBuilder,
|
|
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
|
|
FlightWaypointType.PICKUP_ZONE: LandingZoneBuilder,
|
|
FlightWaypointType.RECOVERY_TANKER: RecoveryTankerBuilder,
|
|
FlightWaypointType.REFUEL: RefuelPointBuilder,
|
|
FlightWaypointType.SPLIT: SplitPointBuilder,
|
|
FlightWaypointType.TARGET_GROUP_LOC: TargetBuilder,
|
|
FlightWaypointType.TARGET_POINT: TargetBuilder,
|
|
}
|
|
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
|
return builder(
|
|
waypoint,
|
|
self.group,
|
|
self.flight,
|
|
self.mission,
|
|
self.time,
|
|
self.mission_data,
|
|
self.unit_map,
|
|
generated_waypoint_index,
|
|
)
|
|
|
|
def _estimate_min_fuel_for(self, waypoints: list[FlightWaypoint]) -> None:
|
|
if self.flight.unit_type.fuel_consumption is None:
|
|
return
|
|
|
|
consumption = self.flight.unit_type.fuel_consumption
|
|
min_fuel: float = consumption.min_safe
|
|
|
|
# The flight plan (in reverse) up to and including the arrival point.
|
|
main_flight_plan: Iterator[FlightWaypoint] = reversed(waypoints)
|
|
try:
|
|
while waypoint := next(main_flight_plan):
|
|
if waypoint.waypoint_type is FlightWaypointType.LANDING_POINT:
|
|
waypoint.min_fuel = min_fuel
|
|
main_flight_plan = itertools.chain([waypoint], main_flight_plan)
|
|
break
|
|
except StopIteration:
|
|
# Some custom flight plan without a landing point. Skip it.
|
|
return
|
|
|
|
for b, a in pairwise(main_flight_plan):
|
|
for_leg = self.flight.flight_plan.fuel_consumption_between_points(a, b)
|
|
if for_leg is None:
|
|
continue
|
|
min_fuel += for_leg
|
|
a.min_fuel = min_fuel
|
|
|
|
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:
|
|
force_delay = False
|
|
if isinstance(self.flight.state, WaitingForStart):
|
|
delay = self.flight.state.time_remaining(self.time)
|
|
elif (
|
|
# The first two clauses capture the flight states that we want to adjust. We
|
|
# don't want to delay any flights that are already in flight or on the
|
|
# runway.
|
|
not self.flight.state.in_flight
|
|
and self.flight.state.spawn_type is not StartType.RUNWAY
|
|
and self.flight.departure.is_fleet
|
|
and not self.flight.client_count
|
|
):
|
|
# https://github.com/dcs-liberation/dcs_liberation/issues/1309
|
|
# Without a delay, AI aircraft will be spawned on the sixpack, which other
|
|
# AI planes of course want to taxi through, deadlocking the carrier deck.
|
|
# Delaying AI carrier deck spawns by one second for some reason causes DCS
|
|
# to spawn those aircraft elsewhere, avoiding the traffic jam.
|
|
delay = timedelta(seconds=1)
|
|
force_delay = True
|
|
else:
|
|
delay = timedelta()
|
|
|
|
if force_delay or self.should_delay_flight():
|
|
if self.should_activate_late():
|
|
# Late activation causes the aircraft to not be spawned
|
|
# until triggered.
|
|
self.set_activation_time(delay)
|
|
elif self.flight.start_type is StartType.COLD:
|
|
# Setting the start time causes the AI to wait until the
|
|
# specified time to begin their startup sequence.
|
|
self.set_startup_time(delay)
|
|
|
|
# And setting *our* waypoint TOT causes the takeoff time to show up in
|
|
# the player's kneeboard.
|
|
waypoint.tot = self.flight.flight_plan.takeoff_time()
|
|
return delay
|
|
|
|
def set_activation_time(self, delay: timedelta) -> None:
|
|
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
|
# mission editor. Waypoint times will be relative to the group
|
|
# activation time rather than in absolute local time. A flight delayed
|
|
# until 09:10 when the overall mission start time is 09:00, with a join
|
|
# time of 09:30 will show the join time as 00:30, not 09:30.
|
|
self.group.late_activation = True
|
|
|
|
activation_trigger = TriggerOnce(
|
|
Event.NoEvent, f"FlightLateActivationTrigger{self.group.id}"
|
|
)
|
|
activation_trigger.add_condition(TimeAfter(seconds=int(delay.total_seconds())))
|
|
|
|
self.prevent_spawn_at_hostile_airbase(activation_trigger)
|
|
activation_trigger.add_action(ActivateGroup(self.group.id))
|
|
self.mission.triggerrules.triggers.append(activation_trigger)
|
|
|
|
def prevent_spawn_at_hostile_airbase(self, trigger: TriggerRule) -> None:
|
|
# Prevent delayed flights from spawning at airbases if they were
|
|
# captured before they've spawned.
|
|
if (airport := self.flight.departure.dcs_airport) is not None:
|
|
trigger.add_condition(
|
|
CoalitionHasAirdrome(
|
|
self.flight.squadron.coalition.coalition_id, airport.id
|
|
)
|
|
)
|
|
|
|
def set_startup_time(self, delay: timedelta) -> None:
|
|
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
|
self.group.uncontrolled = True
|
|
|
|
activation_trigger = TriggerOnce(
|
|
Event.NoEvent, f"FlightStartTrigger{self.group.id}"
|
|
)
|
|
activation_trigger.add_condition(TimeAfter(seconds=int(delay.total_seconds())))
|
|
|
|
self.prevent_spawn_at_hostile_airbase(activation_trigger)
|
|
self.group.add_trigger_action(StartCommand())
|
|
activation_trigger.add_action(AITaskPush(self.group.id, len(self.group.tasks)))
|
|
self.mission.triggerrules.triggers.append(activation_trigger)
|
|
|
|
def should_delay_flight(self) -> bool:
|
|
if not isinstance(self.flight.state, WaitingForStart):
|
|
return False
|
|
|
|
if not self.flight.client_count:
|
|
return True
|
|
|
|
if self.flight.state.time_remaining(self.time) < timedelta(minutes=10):
|
|
# Don't bother delaying client flights with short start delays. Much more
|
|
# than ten minutes starts to eat into fuel a bit more (especially for
|
|
# something fuel limited like a Harrier).
|
|
return False
|
|
|
|
return not self.settings.never_delay_player_flights
|
|
|
|
def should_activate_late(self) -> bool:
|
|
if self.flight.start_type is not StartType.COLD:
|
|
# Avoid spawning aircraft in the air or on the runway until it's
|
|
# time for their mission. Also avoid burning through gas spawning
|
|
# hot aircraft hours before their takeoff time.
|
|
return True
|
|
|
|
if self.flight.departure.is_fleet:
|
|
# Carrier spawns will crowd the carrier deck, especially without
|
|
# super carrier.
|
|
# TODO: Is there enough parking on the supercarrier?
|
|
return True
|
|
|
|
return False
|