From 1a3b8d1dd609e9ff7e5e39fc1c550f1a4c28fd79 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 30 Oct 2021 11:32:29 -0700 Subject: [PATCH] Reduce fuel for fast-forwarded player flights. This only has an effect for aircraft for which we have fuel consumption data, but that's fine. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1681 --- game/ato/flight.py | 19 ++++++-- game/ato/flightstate/flightstate.py | 6 +++ game/ato/flightstate/inflight.py | 26 +++++++++- game/ato/flightstate/loiter.py | 4 ++ game/ato/flightstate/racetrack.py | 4 ++ game/ato/flightstate/takeoff.py | 7 +++ .../aircraft/flightgroupconfigurator.py | 32 ++++++++----- .../aircraft/waypoints/waypointgenerator.py | 27 ++--------- game/utils.py | 4 +- gen/flights/flightplan.py | 48 +++++++++++++++---- 10 files changed, 127 insertions(+), 50 deletions(-) diff --git a/game/ato/flight.py b/game/ato/flight.py index 462360c3..c62dc3c9 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -1,12 +1,13 @@ from __future__ import annotations from datetime import datetime, timedelta -from typing import Any, Optional, List, TYPE_CHECKING +from typing import Any, List, Optional, TYPE_CHECKING + +from dcs.planes import C_101CC, C_101EB, Su_33 from gen.flights.loadouts import Loadout - from .flightroster import FlightRoster -from .flightstate import Uninitialized, FlightState +from .flightstate import FlightState, Uninitialized if TYPE_CHECKING: from game.dcs.aircrafttype import AircraftType @@ -122,6 +123,18 @@ class Flight: self.roster.clear() self.squadron.claim_inventory(-self.count) + def max_takeoff_fuel(self) -> Optional[float]: + # Special case so Su 33 and C101 can take off + unit_type = self.unit_type.dcs_unit_type + if unit_type == Su_33: + if self.flight_type.is_air_to_air: + return Su_33.fuel_max / 2.2 + else: + return Su_33.fuel_max * 0.8 + elif unit_type in {C_101EB, C_101CC}: + return unit_type.fuel_max * 0.5 + return None + def __repr__(self) -> str: if self.custom_name: return f"{self.custom_name} {self.count} x {self.unit_type}" diff --git a/game/ato/flightstate/flightstate.py b/game/ato/flightstate/flightstate.py index a07714cf..41ddf48c 100644 --- a/game/ato/flightstate/flightstate.py +++ b/game/ato/flightstate/flightstate.py @@ -37,3 +37,9 @@ class FlightState(ABC): def a2a_commit_region(self) -> Optional[ThreatPoly]: return None + + def estimate_fuel(self) -> float: + """Returns the estimated remaining fuel **in kilograms**.""" + if (max_takeoff_fuel := self.flight.max_takeoff_fuel()) is not None: + return max_takeoff_fuel + return self.flight.unit_type.dcs_unit_type.fuel_max diff --git a/game/ato/flightstate/inflight.py b/game/ato/flightstate/inflight.py index d8371eb9..dd65a7f8 100644 --- a/game/ato/flightstate/inflight.py +++ b/game/ato/flightstate/inflight.py @@ -11,7 +11,7 @@ from game.ato.flightstate.flightstate import FlightState from game.ato.flightwaypoint import FlightWaypoint from game.ato.flightwaypointtype import FlightWaypointType from game.ato.starttype import StartType -from game.utils import Distance, Speed, meters +from game.utils import Distance, LBS_TO_KG, Speed, meters, pairwise from gen.flights.flightplan import LoiterFlightPlan if TYPE_CHECKING: @@ -83,6 +83,30 @@ class InFlight(FlightState): self.current_waypoint, self.next_waypoint ) + def estimate_fuel_at_current_waypoint(self) -> float: + initial_fuel = super().estimate_fuel() + if self.flight.unit_type.fuel_consumption is None: + return initial_fuel + initial_fuel -= self.flight.unit_type.fuel_consumption.taxi * LBS_TO_KG + waypoints = self.flight.flight_plan.waypoints[: self.waypoint_index + 1] + for a, b in pairwise(waypoints[:-1]): + consumption = self.flight.flight_plan.fuel_consumption_between_points(a, b) + assert consumption is not None + initial_fuel -= consumption * LBS_TO_KG + return initial_fuel + + def estimate_fuel(self) -> float: + initial_fuel = self.estimate_fuel_at_current_waypoint() + ppm = self.flight.flight_plan.fuel_rate_to_between_points( + self.current_waypoint, self.next_waypoint + ) + if ppm is None: + return initial_fuel + position = self.estimate_position() + distance = meters(self.current_waypoint.position.distance_to_point(position)) + consumption = distance.nautical_miles * ppm * LBS_TO_KG + return initial_fuel - consumption + def next_waypoint_state(self) -> FlightState: from game.ato.flightstate.loiter import Loiter from game.ato.flightstate.racetrack import RaceTrack diff --git a/game/ato/flightstate/loiter.py b/game/ato/flightstate/loiter.py index 7adbd2c9..11342fc1 100644 --- a/game/ato/flightstate/loiter.py +++ b/game/ato/flightstate/loiter.py @@ -29,6 +29,10 @@ class Loiter(InFlight): def estimate_speed(self) -> Speed: return self.flight.unit_type.preferred_patrol_speed(self.estimate_altitude()[0]) + def estimate_fuel(self) -> float: + # TODO: Estimate loiter consumption per minute? + return self.estimate_fuel_at_current_waypoint() + def next_waypoint_state(self) -> FlightState: # Do not automatically advance to the next waypoint. Just proceed from the # current one with the normal flying state. diff --git a/game/ato/flightstate/racetrack.py b/game/ato/flightstate/racetrack.py index 7309a04e..45301cc2 100644 --- a/game/ato/flightstate/racetrack.py +++ b/game/ato/flightstate/racetrack.py @@ -40,6 +40,10 @@ class RaceTrack(InFlight): def estimate_speed(self) -> Speed: return self.flight.unit_type.preferred_patrol_speed(self.estimate_altitude()[0]) + def estimate_fuel(self) -> float: + # TODO: Estimate loiter consumption per minute? + return self.estimate_fuel_at_current_waypoint() + def travel_time_between_waypoints(self) -> timedelta: return self.patrol_duration diff --git a/game/ato/flightstate/takeoff.py b/game/ato/flightstate/takeoff.py index fb82131c..905caa89 100644 --- a/game/ato/flightstate/takeoff.py +++ b/game/ato/flightstate/takeoff.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING from .flightstate import FlightState from .inflight import InFlight from ..starttype import StartType +from ...utils import LBS_TO_KG if TYPE_CHECKING: from game.ato.flight import Flight @@ -33,6 +34,12 @@ class Takeoff(FlightState): def spawn_type(self) -> StartType: return StartType.RUNWAY + def estimate_fuel(self) -> float: + initial_fuel = super().estimate_fuel() + if self.flight.unit_type.fuel_consumption is None: + return initial_fuel + return initial_fuel - self.flight.unit_type.fuel_consumption.taxi * LBS_TO_KG + def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool: if ( self.flight.client_count > 0 diff --git a/game/missiongenerator/aircraft/flightgroupconfigurator.py b/game/missiongenerator/aircraft/flightgroupconfigurator.py index d0566c04..7bb30b9c 100644 --- a/game/missiongenerator/aircraft/flightgroupconfigurator.py +++ b/game/missiongenerator/aircraft/flightgroupconfigurator.py @@ -6,8 +6,7 @@ from typing import Any, Optional, TYPE_CHECKING from dcs import Mission from dcs.flyingunit import FlyingUnit -from dcs.planes import C_101CC, C_101EB, F_14B, Su_33 -from dcs.task import CAP +from dcs.planes import F_14B from dcs.unit import Skill from dcs.unitgroup import FlyingGroup @@ -208,14 +207,21 @@ class FlightGroupConfigurator: pylon.equip(self.group, weapon) def setup_fuel(self) -> None: - # Special case so Su 33 and C101 can take off - unit_type = self.flight.unit_type.dcs_unit_type - if unit_type == Su_33: - for unit in self.group.units: - if self.group.task == CAP: - unit.fuel = Su_33.fuel_max / 2.2 - else: - unit.fuel = Su_33.fuel_max * 0.8 - elif unit_type in {C_101EB, C_101CC}: - for unit in self.group.units: - unit.fuel = unit_type.fuel_max * 0.5 + fuel = self.flight.state.estimate_fuel() + if fuel < 0: + logging.warning( + f"Flight {self.flight} is estimated to have no fuel at mission start. " + "This estimate does not account for external fuel tanks. Setting " + "starting fuel to 100kg." + ) + fuel = 100 + for unit, pilot in zip(self.group.units, self.flight.roster.pilots): + if pilot is not None and pilot.player: + unit.fuel = fuel + elif (max_takeoff_fuel := self.flight.max_takeoff_fuel()) is not None: + unit.fuel = max_takeoff_fuel + else: + # pydcs arbitrarily reduces the fuel of in-flight spawns by 10%. We do + # our own tracking, so undo that. + # https://github.com/pydcs/dcs/commit/303a81a8e0c778599fe136dd22cb2ae8123639a6 + unit.fuel = self.flight.unit_type.dcs_unit_type.fuel_max diff --git a/game/missiongenerator/aircraft/waypoints/waypointgenerator.py b/game/missiongenerator/aircraft/waypoints/waypointgenerator.py index 79c41a36..41ebb8cf 100644 --- a/game/missiongenerator/aircraft/waypoints/waypointgenerator.py +++ b/game/missiongenerator/aircraft/waypoints/waypointgenerator.py @@ -18,7 +18,7 @@ from game.ato.starttype import StartType from game.missiongenerator.airsupport import AirSupport from game.settings import Settings from game.theater import ControlPointType -from game.utils import meters, pairwise +from game.utils import pairwise from .baiingress import BaiIngressBuilder from .cargostop import CargoStopBuilder from .casingress import CasIngressBuilder @@ -145,19 +145,6 @@ class WaypointGenerator: if self.flight.unit_type.fuel_consumption is None: return - combat_speed_types = { - FlightWaypointType.INGRESS_BAI, - FlightWaypointType.INGRESS_CAS, - FlightWaypointType.INGRESS_DEAD, - FlightWaypointType.INGRESS_ESCORT, - FlightWaypointType.INGRESS_OCA_AIRCRAFT, - FlightWaypointType.INGRESS_OCA_RUNWAY, - FlightWaypointType.INGRESS_SEAD, - FlightWaypointType.INGRESS_STRIKE, - FlightWaypointType.INGRESS_SWEEP, - FlightWaypointType.SPLIT, - } | set(TARGET_WAYPOINTS) - consumption = self.flight.unit_type.fuel_consumption min_fuel: float = consumption.min_safe @@ -174,14 +161,10 @@ class WaypointGenerator: return for b, a in pairwise(main_flight_plan): - distance = meters(a.position.distance_to_point(b.position)) - if a.waypoint_type is FlightWaypointType.TAKEOFF: - ppm = consumption.climb - elif b.waypoint_type in combat_speed_types: - ppm = consumption.combat - else: - ppm = consumption.cruise - min_fuel += distance.nautical_miles * ppm + 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: diff --git a/game/utils.py b/game/utils.py index 9e3826a6..6d776b85 100644 --- a/game/utils.py +++ b/game/utils.py @@ -5,7 +5,7 @@ import math import random from collections import Iterable from dataclasses import dataclass -from typing import Union, Any, TypeVar +from typing import TypeVar, Union METERS_TO_FEET = 3.28084 FEET_TO_METERS = 1 / METERS_TO_FEET @@ -20,6 +20,8 @@ KPH_TO_MS = 1 / MS_TO_KPH INHG_TO_HPA = 33.86389 INHG_TO_MMHG = 25.400002776728 +LBS_TO_KG = 0.453592 + @dataclass(frozen=True, order=True) class Distance: diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 663aead7..bbc8fb55 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -19,30 +19,30 @@ from dcs.mapping import Point from dcs.unit import Unit from shapely.geometry import Point as ShapelyPoint +from game.ato.flighttype import FlightType +from game.ato.flightwaypoint import FlightWaypoint +from game.ato.flightwaypointtype import FlightWaypointType from game.ato.starttype import StartType from game.data.doctrine import Doctrine from game.dcs.aircrafttype import FuelConsumption -from game.flightplan import IpZoneGeometry, JoinZoneGeometry, HoldZoneGeometry +from game.flightplan import HoldZoneGeometry, IpZoneGeometry, JoinZoneGeometry from game.theater import ( Airfield, + ConflictTheater, ControlPoint, FrontLine, MissionTarget, + NavalControlPoint, SamGroundObject, TheaterGroundObject, - NavalControlPoint, - ConflictTheater, ) from game.theater.theatergroundobject import ( + BuildingGroundObject, EwrGroundObject, NavalGroundObject, - BuildingGroundObject, ) -from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots +from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles from .closestairfields import ObjectiveDistanceCache -from game.ato.flighttype import FlightType -from game.ato.flightwaypointtype import FlightWaypointType -from game.ato.flightwaypoint import FlightWaypoint from .traveltime import GroundSpeed, TravelTime from .waypointbuilder import StrikeTarget, WaypointBuilder @@ -126,6 +126,30 @@ class FlightPlan: def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed: return self.best_speed_between_waypoints(a, b) + @property + def combat_speed_waypoints(self) -> set[FlightWaypoint]: + raise NotImplementedError + + def fuel_consumption_between_points( + self, a: FlightWaypoint, b: FlightWaypoint + ) -> Optional[float]: + ppm = self.fuel_rate_to_between_points(a, b) + if ppm is None: + return None + distance = meters(a.position.distance_to_point(b.position)) + return distance.nautical_miles * ppm + + def fuel_rate_to_between_points( + self, a: FlightWaypoint, b: FlightWaypoint + ) -> Optional[float]: + if self.flight.unit_type.fuel_consumption is None: + return None + if a.waypoint_type is FlightWaypointType.TAKEOFF: + return self.flight.unit_type.fuel_consumption.climb + if b in self.combat_speed_waypoints: + return self.flight.unit_type.fuel_consumption.combat + return self.flight.unit_type.fuel_consumption.cruise + @property def tot_waypoint(self) -> Optional[FlightWaypoint]: """The waypoint that is associated with the package TOT, or None. @@ -343,9 +367,13 @@ class FormationFlightPlan(LoiterFlightPlan): raise NotImplementedError @property - def package_speed_waypoints(self) -> Set[FlightWaypoint]: + def package_speed_waypoints(self) -> set[FlightWaypoint]: raise NotImplementedError + @property + def combat_speed_waypoints(self) -> set[FlightWaypoint]: + return self.package_speed_waypoints + @property def tot_waypoint(self) -> Optional[FlightWaypoint]: raise NotImplementedError @@ -595,7 +623,7 @@ class StrikeFlightPlan(FormationFlightPlan): yield self.bullseye @property - def package_speed_waypoints(self) -> Set[FlightWaypoint]: + def package_speed_waypoints(self) -> set[FlightWaypoint]: return { self.ingress, self.split,