mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Improve unit system support in kneeboards.
* Factor out unit systems. * Add support for more unit systems (nautical and imperial). * Fuel units support. * Data for many more aircraft.
This commit is contained in:
parent
cd97565cce
commit
4b99ae957e
@ -33,7 +33,11 @@ from game.radio.channels import (
|
||||
from game.utils import (
|
||||
Distance,
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL,
|
||||
ImperialUnits,
|
||||
MetricUnits,
|
||||
NauticalUnits,
|
||||
Speed,
|
||||
UnitSystem,
|
||||
feet,
|
||||
knots,
|
||||
kph,
|
||||
@ -154,8 +158,8 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
# main weapon. It'll RTB when it doesn't have gun ammo left.
|
||||
gunfighter: bool
|
||||
|
||||
# If true, kneeboards will be generated in metric units
|
||||
metric_kneeboard: bool
|
||||
# UnitSystem to use for the kneeboard, defaults to Nautical (kt/nm/ft)
|
||||
kneeboard_units: UnitSystem
|
||||
|
||||
# If true, kneeboards will display zulu times
|
||||
utc_kneeboard: bool
|
||||
@ -376,6 +380,13 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
except KeyError:
|
||||
introduction = "No data."
|
||||
|
||||
units_data = data.get("kneeboard_units", "nautical").lower()
|
||||
units: UnitSystem = NauticalUnits()
|
||||
if units_data == "imperial":
|
||||
units = ImperialUnits()
|
||||
if units_data == "metric":
|
||||
units = MetricUnits()
|
||||
|
||||
for variant in data.get("variants", [aircraft.id]):
|
||||
yield AircraftType(
|
||||
dcs_unit_type=aircraft,
|
||||
@ -401,6 +412,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
intra_flight_radio=radio_config.intra_flight,
|
||||
channel_allocator=radio_config.channel_allocator,
|
||||
channel_namer=radio_config.channel_namer,
|
||||
metric_kneeboard=data.get("metric_kneeboard", False),
|
||||
kneeboard_units=units,
|
||||
utc_kneeboard=data.get("utc_kneeboard", False),
|
||||
)
|
||||
|
||||
@ -44,7 +44,7 @@ from game.dcs.aircrafttype import AircraftType
|
||||
from game.radio.radios import RadioFrequency
|
||||
from game.theater import ConflictTheater, LatLon, TheaterGroundObject
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.utils import Distance, meters
|
||||
from game.utils import Distance, UnitSystem, meters, mps, pounds
|
||||
from game.weather import Weather
|
||||
from gen.runways import RunwayData
|
||||
from .aircraft.flightdata import FlightData
|
||||
@ -202,12 +202,12 @@ class FlightPlanBuilder:
|
||||
|
||||
WAYPOINT_DESC_MAX_LEN = 25
|
||||
|
||||
def __init__(self, start_time: datetime.datetime, is_metric: bool) -> None:
|
||||
def __init__(self, start_time: datetime.datetime, units: UnitSystem) -> None:
|
||||
self.start_time = start_time
|
||||
self.rows: List[List[str]] = []
|
||||
self.target_points: List[NumberedWaypoint] = []
|
||||
self.last_waypoint: Optional[FlightWaypoint] = None
|
||||
self.is_metric = is_metric
|
||||
self.units = units
|
||||
|
||||
def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None:
|
||||
if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT:
|
||||
@ -269,32 +269,24 @@ class FlightPlanBuilder:
|
||||
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
|
||||
|
||||
def _format_alt(self, alt: Distance) -> str:
|
||||
if self.is_metric:
|
||||
return f"{alt.meters:.0f} m"
|
||||
else:
|
||||
return f"{alt.feet:.0f} ft"
|
||||
return f"{self.units.distance_short(alt):.0f}"
|
||||
|
||||
def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
|
||||
if self.last_waypoint is None:
|
||||
return "-"
|
||||
|
||||
if self.is_metric:
|
||||
distance = meters(
|
||||
self.last_waypoint.position.distance_to_point(waypoint.position)
|
||||
)
|
||||
return f"{(distance.meters / 1000):.1f} km"
|
||||
else:
|
||||
distance = meters(
|
||||
self.last_waypoint.position.distance_to_point(waypoint.position)
|
||||
)
|
||||
return f"{distance.nautical_miles:.1f} nm"
|
||||
distance = meters(
|
||||
self.last_waypoint.position.distance_to_point(waypoint.position)
|
||||
)
|
||||
|
||||
return f"{self.units.distance_long(distance):.1f}"
|
||||
|
||||
def _waypoint_bearing(self, waypoint: FlightWaypoint) -> str:
|
||||
if self.last_waypoint is None:
|
||||
return "-"
|
||||
bearing = self.last_waypoint.position.heading_between_point(waypoint.position)
|
||||
|
||||
return f"{(bearing):.0f} T"
|
||||
return f"{(bearing):.0f}"
|
||||
|
||||
def _ground_speed(self, waypoint: FlightWaypoint) -> str:
|
||||
if self.last_waypoint is None:
|
||||
@ -310,20 +302,19 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
return "-"
|
||||
|
||||
distance = meters(
|
||||
speed = mps(
|
||||
self.last_waypoint.position.distance_to_point(waypoint.position)
|
||||
/ (waypoint.tot - last_time).total_seconds()
|
||||
)
|
||||
duration = (waypoint.tot - last_time).total_seconds() / 3600
|
||||
if self.is_metric:
|
||||
return f"{int((distance.meters / 1000) / duration)} km/h"
|
||||
else:
|
||||
return f"{int(distance.nautical_miles / duration)} kt"
|
||||
|
||||
@staticmethod
|
||||
def _format_min_fuel(min_fuel: Optional[float]) -> str:
|
||||
return f"{self.units.speed(speed):.0f}"
|
||||
|
||||
def _format_min_fuel(self, min_fuel: Optional[float]) -> str:
|
||||
if min_fuel is None:
|
||||
return ""
|
||||
return str(math.ceil(min_fuel / 100) * 100)
|
||||
|
||||
mass = pounds(min_fuel)
|
||||
return f"{math.ceil(self.units.mass(mass) / 100) * 100:.0f}"
|
||||
|
||||
def build(self) -> List[List[str]]:
|
||||
return self.rows
|
||||
@ -373,13 +364,29 @@ class BriefingPage(KneeboardPage):
|
||||
)
|
||||
|
||||
writer.heading("Flight Plan")
|
||||
flight_plan_builder = FlightPlanBuilder(
|
||||
self.start_time, self.flight.aircraft_type.metric_kneeboard
|
||||
)
|
||||
|
||||
units = self.flight.aircraft_type.kneeboard_units
|
||||
|
||||
flight_plan_builder = FlightPlanBuilder(self.start_time, units)
|
||||
for num, waypoint in enumerate(self.flight.waypoints):
|
||||
flight_plan_builder.add_waypoint(num, waypoint)
|
||||
|
||||
uom_row = [
|
||||
[
|
||||
"",
|
||||
"",
|
||||
units.distance_short_uom,
|
||||
units.distance_long_uom,
|
||||
"T",
|
||||
units.speed_uom,
|
||||
"",
|
||||
"",
|
||||
units.mass_uom,
|
||||
]
|
||||
]
|
||||
|
||||
writer.table(
|
||||
flight_plan_builder.build(),
|
||||
flight_plan_builder.build() + uom_row,
|
||||
headers=[
|
||||
"#",
|
||||
"Action",
|
||||
@ -404,15 +411,18 @@ class BriefingPage(KneeboardPage):
|
||||
)
|
||||
writer.text(f"QNH: {qnh_in_hg} inHg / {qnh_mm_hg} mmHg / {qnh_hpa} hPa")
|
||||
|
||||
writer.table(
|
||||
[
|
||||
fl = self.flight
|
||||
|
||||
if fl.bingo_fuel and fl.joker_fuel:
|
||||
writer.table(
|
||||
[
|
||||
"{}lbs".format(self.flight.bingo_fuel),
|
||||
"{}lbs".format(self.flight.joker_fuel),
|
||||
]
|
||||
],
|
||||
["Bingo", "Joker"],
|
||||
)
|
||||
[
|
||||
f"{units.mass(pounds(fl.bingo_fuel)):.0f} {units.mass_uom}",
|
||||
f"{units.mass(pounds(fl.joker_fuel)):.0f} {units.mass_uom}",
|
||||
]
|
||||
],
|
||||
["Bingo", "Joker"],
|
||||
)
|
||||
|
||||
if any(self.flight.laser_codes):
|
||||
codes: list[list[str]] = []
|
||||
|
||||
167
game/utils.py
167
game/utils.py
@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import itertools
|
||||
import math
|
||||
@ -14,16 +15,149 @@ METERS_TO_FEET = 3.28084
|
||||
FEET_TO_METERS = 1 / METERS_TO_FEET
|
||||
NM_TO_METERS = 1852
|
||||
METERS_TO_NM = 1 / NM_TO_METERS
|
||||
MILES_TO_METERS = 1609.34
|
||||
METERS_TO_MILES = 1 / MILES_TO_METERS
|
||||
|
||||
KNOTS_TO_KPH = 1.852
|
||||
KPH_TO_KNOTS = 1 / KNOTS_TO_KPH
|
||||
MS_TO_KPH = 3.6
|
||||
KPH_TO_MS = 1 / MS_TO_KPH
|
||||
KPH_TO_MPH = 0.621371
|
||||
MPH_TO_KPH = 1 / KPH_TO_MPH
|
||||
|
||||
INHG_TO_HPA = 33.86389
|
||||
INHG_TO_MMHG = 25.400002776728
|
||||
|
||||
LBS_TO_KG = 0.453592
|
||||
KG_TO_LBS = 1 / LBS_TO_KG
|
||||
|
||||
|
||||
class UnitSystem(ABC):
|
||||
@abstractmethod
|
||||
def distance_short(self, dist: Distance) -> float:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def distance_long(self, dist: Distance) -> float:
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def distance_short_uom(self) -> str:
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def distance_long_uom(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def speed(self, speed: Speed) -> float:
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def speed_uom(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def mass(self, mass: Mass) -> float:
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def mass_uom(self) -> str:
|
||||
pass
|
||||
|
||||
|
||||
class NauticalUnits(UnitSystem):
|
||||
def distance_short(self, dist: Distance) -> float:
|
||||
return dist.feet
|
||||
|
||||
def distance_long(self, dist: Distance) -> float:
|
||||
return dist.nautical_miles
|
||||
|
||||
@property
|
||||
def distance_short_uom(self) -> str:
|
||||
return "ft"
|
||||
|
||||
@property
|
||||
def distance_long_uom(self) -> str:
|
||||
return "nm"
|
||||
|
||||
def speed(self, speed: Speed) -> float:
|
||||
return speed.knots
|
||||
|
||||
@property
|
||||
def speed_uom(self) -> str:
|
||||
return "kt"
|
||||
|
||||
def mass(self, mass: Mass) -> float:
|
||||
return mass.pounds
|
||||
|
||||
@property
|
||||
def mass_uom(self) -> str:
|
||||
return "lb"
|
||||
|
||||
|
||||
class MetricUnits(UnitSystem):
|
||||
def distance_short(self, dist: Distance) -> float:
|
||||
return dist.meters
|
||||
|
||||
def distance_long(self, dist: Distance) -> float:
|
||||
return dist.kilometers
|
||||
|
||||
@property
|
||||
def distance_short_uom(self) -> str:
|
||||
return "m"
|
||||
|
||||
@property
|
||||
def distance_long_uom(self) -> str:
|
||||
return "km"
|
||||
|
||||
def speed(self, speed: Speed) -> float:
|
||||
return speed.kph
|
||||
|
||||
@property
|
||||
def speed_uom(self) -> str:
|
||||
return "kph"
|
||||
|
||||
def mass(self, mass: Mass) -> float:
|
||||
return mass.kgs
|
||||
|
||||
@property
|
||||
def mass_uom(self) -> str:
|
||||
return "kg"
|
||||
|
||||
|
||||
class ImperialUnits(UnitSystem):
|
||||
def distance_short(self, dist: Distance) -> float:
|
||||
return dist.feet
|
||||
|
||||
def distance_long(self, dist: Distance) -> float:
|
||||
return dist.miles
|
||||
|
||||
@property
|
||||
def distance_short_uom(self) -> str:
|
||||
return "ft"
|
||||
|
||||
@property
|
||||
def distance_long_uom(self) -> str:
|
||||
return "m"
|
||||
|
||||
def speed(self, speed: Speed) -> float:
|
||||
return speed.mph
|
||||
|
||||
@property
|
||||
def speed_uom(self) -> str:
|
||||
return "mph"
|
||||
|
||||
def mass(self, mass: Mass) -> float:
|
||||
return mass.pounds
|
||||
|
||||
@property
|
||||
def mass_uom(self) -> str:
|
||||
return "lb"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -42,6 +176,14 @@ class Distance:
|
||||
def nautical_miles(self) -> float:
|
||||
return self.distance_in_meters * METERS_TO_NM
|
||||
|
||||
@property
|
||||
def kilometers(self) -> float:
|
||||
return self.distance_in_meters / 1000
|
||||
|
||||
@property
|
||||
def miles(self) -> float:
|
||||
return self.distance_in_meters * METERS_TO_MILES
|
||||
|
||||
@classmethod
|
||||
def from_feet(cls, value: float) -> Distance:
|
||||
return cls(value * FEET_TO_METERS)
|
||||
@ -119,6 +261,10 @@ class Speed:
|
||||
def meters_per_second(self) -> float:
|
||||
return self.speed_in_kph * KPH_TO_MS
|
||||
|
||||
@property
|
||||
def mph(self) -> float:
|
||||
return self.speed_in_kph * KPH_TO_MPH
|
||||
|
||||
def mach(self, altitude: Distance = meters(0)) -> float:
|
||||
c_sound = mach(1, altitude)
|
||||
return self.speed_in_kph / c_sound.kph
|
||||
@ -272,6 +418,27 @@ def inches_hg(value: float) -> Pressure:
|
||||
return Pressure(value)
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Mass:
|
||||
mass_in_kg: float
|
||||
|
||||
@property
|
||||
def pounds(self) -> float:
|
||||
return self.mass_in_kg * KG_TO_LBS
|
||||
|
||||
@property
|
||||
def kgs(self) -> float:
|
||||
return self.mass_in_kg
|
||||
|
||||
|
||||
def pounds(value: float) -> Mass:
|
||||
return Mass(value * LBS_TO_KG)
|
||||
|
||||
|
||||
def kgs(value: float) -> Mass:
|
||||
return Mass(value)
|
||||
|
||||
|
||||
PairwiseT = TypeVar("PairwiseT")
|
||||
|
||||
|
||||
|
||||
@ -29,3 +29,4 @@ radios:
|
||||
channels:
|
||||
type: viggen
|
||||
namer: viggen
|
||||
kneeboard_units: "metric"
|
||||
@ -17,3 +17,4 @@ role: Fighter
|
||||
gunfighter: true
|
||||
variants:
|
||||
"Bf 109 K-4 Kurf\xFCrst": {}
|
||||
kneeboard_units: "metric"
|
||||
@ -28,3 +28,4 @@ role: Fighter
|
||||
gunfighter: true
|
||||
variants:
|
||||
Fw 190 A-8 Anton: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -17,3 +17,4 @@ role: Fighter
|
||||
gunfighter: true
|
||||
variants:
|
||||
Fw 190 D-9 Dora: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -10,3 +10,4 @@ price: 22
|
||||
role: Air-Superiority Fighter
|
||||
variants:
|
||||
J-11A Flanker-L: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -20,3 +20,4 @@ radios:
|
||||
# The R-800L1 doesn't have preset channels, and the other radio is for
|
||||
# communications with FAC and ground units, which don't currently have
|
||||
# radios assigned, so no channels to configure.
|
||||
kneeboard_units: "metric"
|
||||
@ -12,3 +12,4 @@ role: Light Attack
|
||||
gunfighter: true
|
||||
variants:
|
||||
L-39ZA Albatros: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -19,6 +19,6 @@ manufacturer: Mil
|
||||
origin: USSR/Russia
|
||||
price: 14
|
||||
role: Attack/Transport
|
||||
metric_kneeboard: true
|
||||
kneeboard_units: "metric"
|
||||
variants:
|
||||
Mi-24P Hind-F: {}
|
||||
|
||||
@ -10,3 +10,4 @@ price: 5
|
||||
role: Transport/Light Attack
|
||||
variants:
|
||||
Mi-8MTV2 Hip: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -21,3 +21,4 @@ variants:
|
||||
radios:
|
||||
intra_flight: RSI-6K HF
|
||||
inter_flight: RSI-6K HF
|
||||
kneeboard_units: "metric"
|
||||
@ -27,3 +27,4 @@ radios:
|
||||
channels:
|
||||
type: farmer
|
||||
namer: single
|
||||
kneeboard_units: "metric"
|
||||
@ -27,3 +27,4 @@ radios:
|
||||
namer: single
|
||||
intra_flight_radio_index: 1
|
||||
inter_flight_radio_index: 1
|
||||
kneeboard_units: "metric"
|
||||
@ -28,3 +28,4 @@ role: Multirole Fighter
|
||||
max_range: 150
|
||||
variants:
|
||||
MiG-29A Fulcrum-A: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -28,3 +28,4 @@ role: Multirole Fighter
|
||||
max_range: 150
|
||||
variants:
|
||||
MiG-29G Fulcrum-A: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -28,3 +28,4 @@ role: Multirole Fighter
|
||||
max_range: 150
|
||||
variants:
|
||||
MiG-29S Fulcrum-C: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -6,3 +6,4 @@ price: 6
|
||||
role: Light Bomber, Fighter Bomber, Night Fighter, Maritime Strike Aircraft, Photo Recon Aircraft
|
||||
variants:
|
||||
MosquitoFBMkVI: {}
|
||||
kneeboard_units: "imperial"
|
||||
@ -29,3 +29,4 @@ radios:
|
||||
channels:
|
||||
type: SCR-522
|
||||
namer: SCR-522
|
||||
kneeboard_units: "imperial"
|
||||
@ -29,3 +29,4 @@ radios:
|
||||
channels:
|
||||
type: SCR-522
|
||||
namer: SCR-522
|
||||
kneeboard_units: "imperial"
|
||||
@ -28,4 +28,5 @@ radios:
|
||||
inter_flight: SCR522
|
||||
channels:
|
||||
type: SCR-522
|
||||
namer: SCR-522
|
||||
namer: SCR-522
|
||||
kneeboard_units: "imperial"
|
||||
@ -30,3 +30,4 @@ radios:
|
||||
channels:
|
||||
type: SCR-522
|
||||
namer: SCR-522
|
||||
kneeboard_units: "imperial"
|
||||
@ -30,3 +30,4 @@ radios:
|
||||
channels:
|
||||
type: SCR-522
|
||||
namer: SCR-522
|
||||
kneeboard_units: "imperial"
|
||||
@ -15,3 +15,4 @@ price: 5
|
||||
role: Light Attack
|
||||
variants:
|
||||
SA 342L Gazelle: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -18,3 +18,4 @@ variants:
|
||||
introduced: 1974
|
||||
manufacturer: Westland
|
||||
SA 342M Gazelle: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -1,3 +1,4 @@
|
||||
price: 4
|
||||
variants:
|
||||
SA342Minigun: null
|
||||
kneeboard_units: "metric"
|
||||
@ -15,3 +15,4 @@ price: 8
|
||||
role: Light Attack
|
||||
variants:
|
||||
SA 342M Gazelle Mistral: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -41,3 +41,4 @@ role: Fighter
|
||||
gunfighter: true
|
||||
variants:
|
||||
Spitfire LF Mk IX: {}
|
||||
kneeboard_units: "imperial"
|
||||
@ -41,3 +41,4 @@ role: Fighter
|
||||
gunfighter: true
|
||||
variants:
|
||||
Spitfire LF Mk IX (Clipped Wings): {}
|
||||
kneeboard_units: "imperial"
|
||||
@ -14,3 +14,4 @@ role: Close Air Support/Attack
|
||||
max_range: 200
|
||||
variants:
|
||||
Su-25 Frogfoot: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -14,3 +14,4 @@ role: Close Air Support/Attack
|
||||
max_range: 200
|
||||
variants:
|
||||
Su-25T Frogfoot: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -17,3 +17,4 @@ role: Air-Superiority Fighter
|
||||
max_range: 300
|
||||
variants:
|
||||
Su-27 Flanker-B: {}
|
||||
kneeboard_units: "metric"
|
||||
@ -29,3 +29,4 @@ variants:
|
||||
origin: China
|
||||
role: Carrier-based Multirole Fighter
|
||||
Su-33 Flanker-D: {}
|
||||
kneeboard_units: "metric"
|
||||
Loading…
x
Reference in New Issue
Block a user