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 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}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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