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
This commit is contained in:
Dan Albert 2021-10-30 11:32:29 -07:00
parent b2cbf4b6f4
commit 1a3b8d1dd6
10 changed files with 127 additions and 50 deletions

View File

@ -1,12 +1,13 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta 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 gen.flights.loadouts import Loadout
from .flightroster import FlightRoster from .flightroster import FlightRoster
from .flightstate import Uninitialized, FlightState from .flightstate import FlightState, Uninitialized
if TYPE_CHECKING: if TYPE_CHECKING:
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
@ -122,6 +123,18 @@ class Flight:
self.roster.clear() self.roster.clear()
self.squadron.claim_inventory(-self.count) 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: def __repr__(self) -> str:
if self.custom_name: if self.custom_name:
return f"{self.custom_name} {self.count} x {self.unit_type}" return f"{self.custom_name} {self.count} x {self.unit_type}"

View File

@ -37,3 +37,9 @@ class FlightState(ABC):
def a2a_commit_region(self) -> Optional[ThreatPoly]: def a2a_commit_region(self) -> Optional[ThreatPoly]:
return None 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

View File

@ -11,7 +11,7 @@ from game.ato.flightstate.flightstate import FlightState
from game.ato.flightwaypoint import FlightWaypoint from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType 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 from gen.flights.flightplan import LoiterFlightPlan
if TYPE_CHECKING: if TYPE_CHECKING:
@ -83,6 +83,30 @@ class InFlight(FlightState):
self.current_waypoint, self.next_waypoint 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: def next_waypoint_state(self) -> FlightState:
from game.ato.flightstate.loiter import Loiter from game.ato.flightstate.loiter import Loiter
from game.ato.flightstate.racetrack import RaceTrack from game.ato.flightstate.racetrack import RaceTrack

View File

@ -29,6 +29,10 @@ class Loiter(InFlight):
def estimate_speed(self) -> Speed: def estimate_speed(self) -> Speed:
return self.flight.unit_type.preferred_patrol_speed(self.estimate_altitude()[0]) 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: def next_waypoint_state(self) -> FlightState:
# Do not automatically advance to the next waypoint. Just proceed from the # Do not automatically advance to the next waypoint. Just proceed from the
# current one with the normal flying state. # current one with the normal flying state.

View File

@ -40,6 +40,10 @@ class RaceTrack(InFlight):
def estimate_speed(self) -> Speed: def estimate_speed(self) -> Speed:
return self.flight.unit_type.preferred_patrol_speed(self.estimate_altitude()[0]) 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: def travel_time_between_waypoints(self) -> timedelta:
return self.patrol_duration return self.patrol_duration

View File

@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
from .flightstate import FlightState from .flightstate import FlightState
from .inflight import InFlight from .inflight import InFlight
from ..starttype import StartType from ..starttype import StartType
from ...utils import LBS_TO_KG
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato.flight import Flight from game.ato.flight import Flight
@ -33,6 +34,12 @@ class Takeoff(FlightState):
def spawn_type(self) -> StartType: def spawn_type(self) -> StartType:
return StartType.RUNWAY 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: def should_halt_sim(self, enemy_aircraft_coverage: AircraftEngagementZones) -> bool:
if ( if (
self.flight.client_count > 0 self.flight.client_count > 0

View File

@ -6,8 +6,7 @@ from typing import Any, Optional, TYPE_CHECKING
from dcs import Mission from dcs import Mission
from dcs.flyingunit import FlyingUnit from dcs.flyingunit import FlyingUnit
from dcs.planes import C_101CC, C_101EB, F_14B, Su_33 from dcs.planes import F_14B
from dcs.task import CAP
from dcs.unit import Skill from dcs.unit import Skill
from dcs.unitgroup import FlyingGroup from dcs.unitgroup import FlyingGroup
@ -208,14 +207,21 @@ class FlightGroupConfigurator:
pylon.equip(self.group, weapon) pylon.equip(self.group, weapon)
def setup_fuel(self) -> None: def setup_fuel(self) -> None:
# Special case so Su 33 and C101 can take off fuel = self.flight.state.estimate_fuel()
unit_type = self.flight.unit_type.dcs_unit_type if fuel < 0:
if unit_type == Su_33: logging.warning(
for unit in self.group.units: f"Flight {self.flight} is estimated to have no fuel at mission start. "
if self.group.task == CAP: "This estimate does not account for external fuel tanks. Setting "
unit.fuel = Su_33.fuel_max / 2.2 "starting fuel to 100kg."
else: )
unit.fuel = Su_33.fuel_max * 0.8 fuel = 100
elif unit_type in {C_101EB, C_101CC}: for unit, pilot in zip(self.group.units, self.flight.roster.pilots):
for unit in self.group.units: if pilot is not None and pilot.player:
unit.fuel = unit_type.fuel_max * 0.5 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

View File

@ -18,7 +18,7 @@ from game.ato.starttype import StartType
from game.missiongenerator.airsupport import AirSupport from game.missiongenerator.airsupport import AirSupport
from game.settings import Settings from game.settings import Settings
from game.theater import ControlPointType from game.theater import ControlPointType
from game.utils import meters, pairwise from game.utils import pairwise
from .baiingress import BaiIngressBuilder from .baiingress import BaiIngressBuilder
from .cargostop import CargoStopBuilder from .cargostop import CargoStopBuilder
from .casingress import CasIngressBuilder from .casingress import CasIngressBuilder
@ -145,19 +145,6 @@ class WaypointGenerator:
if self.flight.unit_type.fuel_consumption is None: if self.flight.unit_type.fuel_consumption is None:
return 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 consumption = self.flight.unit_type.fuel_consumption
min_fuel: float = consumption.min_safe min_fuel: float = consumption.min_safe
@ -174,14 +161,10 @@ class WaypointGenerator:
return return
for b, a in pairwise(main_flight_plan): for b, a in pairwise(main_flight_plan):
distance = meters(a.position.distance_to_point(b.position)) for_leg = self.flight.flight_plan.fuel_consumption_between_points(a, b)
if a.waypoint_type is FlightWaypointType.TAKEOFF: if for_leg is None:
ppm = consumption.climb continue
elif b.waypoint_type in combat_speed_types: min_fuel += for_leg
ppm = consumption.combat
else:
ppm = consumption.cruise
min_fuel += distance.nautical_miles * ppm
a.min_fuel = min_fuel a.min_fuel = min_fuel
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta: def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:

View File

@ -5,7 +5,7 @@ import math
import random import random
from collections import Iterable from collections import Iterable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Union, Any, TypeVar from typing import TypeVar, Union
METERS_TO_FEET = 3.28084 METERS_TO_FEET = 3.28084
FEET_TO_METERS = 1 / METERS_TO_FEET 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_HPA = 33.86389
INHG_TO_MMHG = 25.400002776728 INHG_TO_MMHG = 25.400002776728
LBS_TO_KG = 0.453592
@dataclass(frozen=True, order=True) @dataclass(frozen=True, order=True)
class Distance: class Distance:

View File

@ -19,30 +19,30 @@ from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
from shapely.geometry import Point as ShapelyPoint 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.ato.starttype import StartType
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.dcs.aircrafttype import FuelConsumption from game.dcs.aircrafttype import FuelConsumption
from game.flightplan import IpZoneGeometry, JoinZoneGeometry, HoldZoneGeometry from game.flightplan import HoldZoneGeometry, IpZoneGeometry, JoinZoneGeometry
from game.theater import ( from game.theater import (
Airfield, Airfield,
ConflictTheater,
ControlPoint, ControlPoint,
FrontLine, FrontLine,
MissionTarget, MissionTarget,
NavalControlPoint,
SamGroundObject, SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
NavalControlPoint,
ConflictTheater,
) )
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject,
EwrGroundObject, EwrGroundObject,
NavalGroundObject, 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 .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 .traveltime import GroundSpeed, TravelTime
from .waypointbuilder import StrikeTarget, WaypointBuilder from .waypointbuilder import StrikeTarget, WaypointBuilder
@ -126,6 +126,30 @@ class FlightPlan:
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed: def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
return self.best_speed_between_waypoints(a, b) 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 @property
def tot_waypoint(self) -> Optional[FlightWaypoint]: def tot_waypoint(self) -> Optional[FlightWaypoint]:
"""The waypoint that is associated with the package TOT, or None. """The waypoint that is associated with the package TOT, or None.
@ -343,9 +367,13 @@ class FormationFlightPlan(LoiterFlightPlan):
raise NotImplementedError raise NotImplementedError
@property @property
def package_speed_waypoints(self) -> Set[FlightWaypoint]: def package_speed_waypoints(self) -> set[FlightWaypoint]:
raise NotImplementedError raise NotImplementedError
@property
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
return self.package_speed_waypoints
@property @property
def tot_waypoint(self) -> Optional[FlightWaypoint]: def tot_waypoint(self) -> Optional[FlightWaypoint]:
raise NotImplementedError raise NotImplementedError
@ -595,7 +623,7 @@ class StrikeFlightPlan(FormationFlightPlan):
yield self.bullseye yield self.bullseye
@property @property
def package_speed_waypoints(self) -> Set[FlightWaypoint]: def package_speed_waypoints(self) -> set[FlightWaypoint]:
return { return {
self.ingress, self.ingress,
self.split, self.split,