mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Add minimum fuel per waypoint on the kneeboard.
This commit is contained in:
parent
3c90a92641
commit
c11c6f40d5
@ -105,6 +105,35 @@ class PatrolConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FuelConsumption:
|
||||||
|
#: The estimated taxi fuel requirement, in pounds.
|
||||||
|
taxi: int
|
||||||
|
|
||||||
|
#: The estimated fuel consumption for a takeoff climb, in pounds per nautical mile.
|
||||||
|
climb: float
|
||||||
|
|
||||||
|
#: The estimated fuel consumption for cruising, in pounds per nautical mile.
|
||||||
|
cruise: float
|
||||||
|
|
||||||
|
#: The estimated fuel consumption for combat speeds, in pounds per nautical mile.
|
||||||
|
combat: float
|
||||||
|
|
||||||
|
#: The minimum amount of fuel that the aircraft should land with, in pounds. This is
|
||||||
|
#: a reserve amount for landing delays or emergencies.
|
||||||
|
min_safe: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data: dict[str, Any]) -> FuelConsumption:
|
||||||
|
return FuelConsumption(
|
||||||
|
int(data["taxi"]),
|
||||||
|
float(data["climb_ppm"]),
|
||||||
|
float(data["cruise_ppm"]),
|
||||||
|
float(data["combat_ppm"]),
|
||||||
|
int(data["min_safe"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO: Split into PlaneType and HelicopterType?
|
# TODO: Split into PlaneType and HelicopterType?
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AircraftType(UnitType[Type[FlyingType]]):
|
class AircraftType(UnitType[Type[FlyingType]]):
|
||||||
@ -124,6 +153,8 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
#: planner will consider this aircraft usable for a mission.
|
#: planner will consider this aircraft usable for a mission.
|
||||||
max_mission_range: Distance
|
max_mission_range: Distance
|
||||||
|
|
||||||
|
fuel_consumption: Optional[FuelConsumption]
|
||||||
|
|
||||||
intra_flight_radio: Optional[Radio]
|
intra_flight_radio: Optional[Radio]
|
||||||
channel_allocator: Optional[RadioChannelAllocator]
|
channel_allocator: Optional[RadioChannelAllocator]
|
||||||
channel_namer: Type[ChannelNamer]
|
channel_namer: Type[ChannelNamer]
|
||||||
@ -246,6 +277,14 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
f"{mission_range.nautical_miles}NM"
|
f"{mission_range.nautical_miles}NM"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fuel_data = data.get("fuel")
|
||||||
|
if fuel_data is not None:
|
||||||
|
fuel_consumption: Optional[FuelConsumption] = FuelConsumption.from_data(
|
||||||
|
fuel_data
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fuel_consumption = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
introduction = data["introduced"]
|
introduction = data["introduced"]
|
||||||
if introduction is None:
|
if introduction is None:
|
||||||
@ -274,6 +313,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
patrol_altitude=patrol_config.altitude,
|
patrol_altitude=patrol_config.altitude,
|
||||||
patrol_speed=patrol_config.speed,
|
patrol_speed=patrol_config.speed,
|
||||||
max_mission_range=mission_range,
|
max_mission_range=mission_range,
|
||||||
|
fuel_consumption=fuel_consumption,
|
||||||
intra_flight_radio=radio_config.intra_flight,
|
intra_flight_radio=radio_config.intra_flight,
|
||||||
channel_allocator=radio_config.channel_allocator,
|
channel_allocator=radio_config.channel_allocator,
|
||||||
channel_namer=radio_config.channel_namer,
|
channel_namer=radio_config.channel_namer,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import itertools
|
|||||||
import math
|
import math
|
||||||
from collections import Iterable
|
from collections import Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Union, Any
|
from typing import Union, Any, TypeVar
|
||||||
|
|
||||||
METERS_TO_FEET = 3.28084
|
METERS_TO_FEET = 3.28084
|
||||||
FEET_TO_METERS = 1 / METERS_TO_FEET
|
FEET_TO_METERS = 1 / METERS_TO_FEET
|
||||||
@ -205,7 +205,10 @@ def inches_hg(value: float) -> Pressure:
|
|||||||
return Pressure(value)
|
return Pressure(value)
|
||||||
|
|
||||||
|
|
||||||
def pairwise(iterable: Iterable[Any]) -> Iterable[tuple[Any, Any]]:
|
PairwiseT = TypeVar("PairwiseT")
|
||||||
|
|
||||||
|
|
||||||
|
def pairwise(iterable: Iterable[PairwiseT]) -> Iterable[tuple[PairwiseT, PairwiseT]]:
|
||||||
"""
|
"""
|
||||||
itertools recipe
|
itertools recipe
|
||||||
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@ -80,7 +81,7 @@ from game.theater.missiontarget import MissionTarget
|
|||||||
from game.theater.theatergroundobject import TheaterGroundObject
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
from game.transfers import MultiGroupTransport
|
from game.transfers import MultiGroupTransport
|
||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
from game.utils import Distance, meters, nautical_miles
|
from game.utils import Distance, meters, nautical_miles, pairwise
|
||||||
from gen.ato import AirTaskingOrder, Package
|
from gen.ato import AirTaskingOrder, Package
|
||||||
from gen.callsigns import create_group_callsign_from_unit
|
from gen.callsigns import create_group_callsign_from_unit
|
||||||
from gen.flights.flight import (
|
from gen.flights.flight import (
|
||||||
@ -1194,8 +1195,57 @@ class AircraftConflictGenerator:
|
|||||||
).build()
|
).build()
|
||||||
|
|
||||||
# Set here rather than when the FlightData is created so they waypoints
|
# Set here rather than when the FlightData is created so they waypoints
|
||||||
# have their TOTs set.
|
# have their TOTs and fuel minimums set. Once we're more confident in our fuel
|
||||||
self.flights[-1].waypoints = [takeoff_point] + flight.points
|
# 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.
|
||||||
|
waypoints = [takeoff_point] + flight.points
|
||||||
|
self._estimate_min_fuel_for(flight, waypoints)
|
||||||
|
self.flights[-1].waypoints = waypoints
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _estimate_min_fuel_for(flight: Flight, waypoints: list[FlightWaypoint]) -> None:
|
||||||
|
if 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 = 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 = 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):
|
||||||
|
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
|
||||||
|
a.min_fuel = min_fuel
|
||||||
|
|
||||||
def should_delay_flight(self, flight: Flight, start_time: timedelta) -> bool:
|
def should_delay_flight(self, flight: Flight, start_time: timedelta) -> bool:
|
||||||
if start_time.total_seconds() <= 0:
|
if start_time.total_seconds() <= 0:
|
||||||
|
|||||||
@ -2,13 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List, Optional, TYPE_CHECKING, Union, Sequence
|
from typing import List, Optional, TYPE_CHECKING, Union, Sequence, Any
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.point import MovingPoint, PointAction
|
from dcs.point import MovingPoint, PointAction
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
|
from game.savecompat import has_save_compat_for
|
||||||
from game.squadrons import Pilot, Squadron
|
from game.squadrons import Pilot, Squadron
|
||||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||||
from game.utils import Distance, meters
|
from game.utils import Distance, meters
|
||||||
@ -138,7 +139,7 @@ class FlightWaypoint:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
waypoint_type: The waypoint type.
|
waypoint_type: The waypoint type.
|
||||||
x: X cooidinate of the waypoint.
|
x: X coordinate of the waypoint.
|
||||||
y: Y coordinate of the waypoint.
|
y: Y coordinate of the waypoint.
|
||||||
alt: Altitude of the waypoint. By default this is AGL, but it can be
|
alt: Altitude of the waypoint. By default this is AGL, but it can be
|
||||||
changed to MSL by setting alt_type to "RADIO".
|
changed to MSL by setting alt_type to "RADIO".
|
||||||
@ -158,6 +159,8 @@ class FlightWaypoint:
|
|||||||
self.pretty_name = ""
|
self.pretty_name = ""
|
||||||
self.only_for_player = False
|
self.only_for_player = False
|
||||||
self.flyover = False
|
self.flyover = False
|
||||||
|
# The minimum amount of fuel remaining at this waypoint in pounds.
|
||||||
|
self.min_fuel: Optional[float] = None
|
||||||
|
|
||||||
# These are set very late by the air conflict generator (part of mission
|
# These are set very late by the air conflict generator (part of mission
|
||||||
# generation). We do it late so that we don't need to propagate changes
|
# generation). We do it late so that we don't need to propagate changes
|
||||||
@ -166,6 +169,12 @@ class FlightWaypoint:
|
|||||||
self.tot: Optional[timedelta] = None
|
self.tot: Optional[timedelta] = None
|
||||||
self.departure_time: Optional[timedelta] = None
|
self.departure_time: Optional[timedelta] = None
|
||||||
|
|
||||||
|
@has_save_compat_for(5)
|
||||||
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
|
if "min_fuel" not in state:
|
||||||
|
state["min_fuel"] = None
|
||||||
|
self.__dict__.update(state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self) -> Point:
|
def position(self) -> Point:
|
||||||
return Point(self.x, self.y)
|
return Point(self.x, self.y)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ from dcs.unit import Unit
|
|||||||
from shapely.geometry import Point as ShapelyPoint
|
from shapely.geometry import Point as ShapelyPoint
|
||||||
|
|
||||||
from game.data.doctrine import Doctrine
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.dcs.aircrafttype import FuelConsumption
|
||||||
from game.flightplan import IpZoneGeometry, JoinZoneGeometry, HoldZoneGeometry
|
from game.flightplan import IpZoneGeometry, JoinZoneGeometry, HoldZoneGeometry
|
||||||
from game.theater import (
|
from game.theater import (
|
||||||
Airfield,
|
Airfield,
|
||||||
@ -138,6 +139,17 @@ class FlightPlan:
|
|||||||
@cached_property
|
@cached_property
|
||||||
def bingo_fuel(self) -> int:
|
def bingo_fuel(self) -> int:
|
||||||
"""Bingo fuel value for the FlightPlan"""
|
"""Bingo fuel value for the FlightPlan"""
|
||||||
|
if (fuel := self.flight.unit_type.fuel_consumption) is not None:
|
||||||
|
return self._bingo_estimate(fuel)
|
||||||
|
return self._legacy_bingo_estimate()
|
||||||
|
|
||||||
|
def _bingo_estimate(self, fuel: FuelConsumption) -> int:
|
||||||
|
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||||
|
fuel_consumed = fuel.cruise * distance_to_arrival.nautical_miles
|
||||||
|
bingo = fuel_consumed + fuel.min_safe
|
||||||
|
return math.ceil(bingo / 100) * 100
|
||||||
|
|
||||||
|
def _legacy_bingo_estimate(self) -> int:
|
||||||
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||||
|
|
||||||
bingo = 1000.0 # Minimum Emergency Fuel
|
bingo = 1000.0 # Minimum Emergency Fuel
|
||||||
|
|||||||
@ -23,6 +23,7 @@ only be added per airframe, so PvP missions where each side have the same
|
|||||||
aircraft will be able to see the enemy's kneeboard for the same airframe.
|
aircraft will be able to see the enemy's kneeboard for the same airframe.
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
|
import math
|
||||||
import textwrap
|
import textwrap
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@ -39,8 +40,8 @@ from game.db import unit_type_from_name
|
|||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
|
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
|
||||||
from game.theater.bullseye import Bullseye
|
from game.theater.bullseye import Bullseye
|
||||||
from game.weather import Weather
|
|
||||||
from game.utils import meters
|
from game.utils import meters
|
||||||
|
from game.weather import Weather
|
||||||
from .aircraft import FlightData
|
from .aircraft import FlightData
|
||||||
from .airsupportgen import AwacsInfo, TankerInfo
|
from .airsupportgen import AwacsInfo, TankerInfo
|
||||||
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
|
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
|
||||||
@ -111,12 +112,17 @@ class KneeboardPageWriter:
|
|||||||
self.text(text, font=self.heading_font, fill=self.foreground_fill)
|
self.text(text, font=self.heading_font, fill=self.foreground_fill)
|
||||||
|
|
||||||
def table(
|
def table(
|
||||||
self, cells: List[List[str]], headers: Optional[List[str]] = None
|
self,
|
||||||
|
cells: List[List[str]],
|
||||||
|
headers: Optional[List[str]] = None,
|
||||||
|
font: Optional[ImageFont.FreeTypeFont] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = []
|
headers = []
|
||||||
|
if font is None:
|
||||||
|
font = self.table_font
|
||||||
table = tabulate(cells, headers=headers, numalign="right")
|
table = tabulate(cells, headers=headers, numalign="right")
|
||||||
self.text(table, font=self.table_font, fill=self.foreground_fill)
|
self.text(table, font, fill=self.foreground_fill)
|
||||||
|
|
||||||
def write(self, path: Path) -> None:
|
def write(self, path: Path) -> None:
|
||||||
self.image.save(path)
|
self.image.save(path)
|
||||||
@ -199,6 +205,7 @@ class FlightPlanBuilder:
|
|||||||
self._ground_speed(self.target_points[0].waypoint),
|
self._ground_speed(self.target_points[0].waypoint),
|
||||||
self._format_time(self.target_points[0].waypoint.tot),
|
self._format_time(self.target_points[0].waypoint.tot),
|
||||||
self._format_time(self.target_points[0].waypoint.departure_time),
|
self._format_time(self.target_points[0].waypoint.departure_time),
|
||||||
|
self._format_min_fuel(self.target_points[0].waypoint.min_fuel),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.last_waypoint = self.target_points[-1].waypoint
|
self.last_waypoint = self.target_points[-1].waypoint
|
||||||
@ -216,6 +223,7 @@ class FlightPlanBuilder:
|
|||||||
self._ground_speed(waypoint.waypoint),
|
self._ground_speed(waypoint.waypoint),
|
||||||
self._format_time(waypoint.waypoint.tot),
|
self._format_time(waypoint.waypoint.tot),
|
||||||
self._format_time(waypoint.waypoint.departure_time),
|
self._format_time(waypoint.waypoint.departure_time),
|
||||||
|
self._format_min_fuel(waypoint.waypoint.min_fuel),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -254,6 +262,12 @@ class FlightPlanBuilder:
|
|||||||
duration = (waypoint.tot - last_time).total_seconds() / 3600
|
duration = (waypoint.tot - last_time).total_seconds() / 3600
|
||||||
return f"{int(distance.nautical_miles / duration)} kt"
|
return f"{int(distance.nautical_miles / duration)} kt"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_min_fuel(min_fuel: Optional[float]) -> str:
|
||||||
|
if min_fuel is None:
|
||||||
|
return ""
|
||||||
|
return str(math.ceil(min_fuel / 100) * 100)
|
||||||
|
|
||||||
def build(self) -> List[List[str]]:
|
def build(self) -> List[List[str]]:
|
||||||
return self.rows
|
return self.rows
|
||||||
|
|
||||||
@ -276,6 +290,11 @@ class BriefingPage(KneeboardPage):
|
|||||||
self.weather = weather
|
self.weather = weather
|
||||||
self.start_time = start_time
|
self.start_time = start_time
|
||||||
self.dark_kneeboard = dark_kneeboard
|
self.dark_kneeboard = dark_kneeboard
|
||||||
|
self.flight_plan_font = ImageFont.truetype(
|
||||||
|
"resources/fonts/Inconsolata.otf",
|
||||||
|
16,
|
||||||
|
layout_engine=ImageFont.LAYOUT_BASIC,
|
||||||
|
)
|
||||||
|
|
||||||
def write(self, path: Path) -> None:
|
def write(self, path: Path) -> None:
|
||||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||||
@ -302,7 +321,17 @@ class BriefingPage(KneeboardPage):
|
|||||||
flight_plan_builder.add_waypoint(num, waypoint)
|
flight_plan_builder.add_waypoint(num, waypoint)
|
||||||
writer.table(
|
writer.table(
|
||||||
flight_plan_builder.build(),
|
flight_plan_builder.build(),
|
||||||
headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"],
|
headers=[
|
||||||
|
"#",
|
||||||
|
"Action",
|
||||||
|
"Alt",
|
||||||
|
"Dist",
|
||||||
|
"GSPD",
|
||||||
|
"Time",
|
||||||
|
"Departure",
|
||||||
|
"Min fuel",
|
||||||
|
],
|
||||||
|
font=self.flight_plan_font,
|
||||||
)
|
)
|
||||||
|
|
||||||
writer.text(f"Bullseye: {self.bullseye.to_lat_lon(self.theater).format_dms()}")
|
writer.text(f"Bullseye: {self.bullseye.to_lat_lon(self.theater).format_dms()}")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user