Merge branch 'develop' into helipads

# Conflicts:
#	game/game.py
#	game/operation/operation.py
#	game/theater/conflicttheater.py
#	game/theater/controlpoint.py
#	gen/groundobjectsgen.py
#	resources/campaigns/golan_heights_lite.miz
This commit is contained in:
Khopa
2021-08-02 19:34:05 +02:00
408 changed files with 9630 additions and 5172 deletions

View File

@@ -29,7 +29,7 @@ from game.radio.channels import (
ViggenRadioChannelAllocator,
NoOpChannelAllocator,
)
from game.utils import Distance, Speed, feet, kph, knots
from game.utils import Distance, Speed, feet, kph, knots, nautical_miles
if TYPE_CHECKING:
from gen.aircraft import FlightData
@@ -98,7 +98,7 @@ class PatrolConfig:
@classmethod
def from_data(cls, data: dict[str, Any]) -> PatrolConfig:
altitude = data.get("altitude", None)
speed = data.get("altitude", None)
speed = data.get("speed", None)
return PatrolConfig(
feet(altitude) if altitude is not None else None,
knots(speed) if speed is not None else None,
@@ -106,18 +106,55 @@ class PatrolConfig:
@dataclass(frozen=True)
class AircraftType(UnitType[FlyingType]):
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?
@dataclass(frozen=True)
class AircraftType(UnitType[Type[FlyingType]]):
carrier_capable: bool
lha_capable: bool
always_keeps_gun: bool
# If true, the aircraft does not use the guns as the last resort weapons, but as a main weapon.
# It'll RTB when it doesn't have gun ammo left.
# If true, the aircraft does not use the guns as the last resort weapons, but as a
# main weapon. It'll RTB when it doesn't have gun ammo left.
gunfighter: bool
max_group_size: int
patrol_altitude: Optional[Distance]
patrol_speed: Optional[Speed]
#: The maximum range between the origin airfield and the target for which the auto-
#: planner will consider this aircraft usable for a mission.
max_mission_range: Distance
fuel_consumption: Optional[FuelConsumption]
intra_flight_radio: Optional[Radio]
channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer]
@@ -147,13 +184,52 @@ class AircraftType(UnitType[FlyingType]):
def max_speed(self) -> Speed:
return kph(self.dcs_unit_type.max_speed)
@property
def preferred_patrol_altitude(self) -> Distance:
if self.patrol_altitude:
return self.patrol_altitude
else:
# Estimate based on max speed.
# Aircaft with max speed 600 kph will prefer patrol at 10 000 ft
# Aircraft with max speed 2800 kph will prefer pratrol at 33 000 ft
altitude_for_lowest_speed = feet(10 * 1000)
altitude_for_highest_speed = feet(33 * 1000)
lowest_speed = kph(600)
highest_speed = kph(2800)
factor = (self.max_speed - lowest_speed).kph / (
highest_speed - lowest_speed
).kph
altitude = (
altitude_for_lowest_speed
+ (altitude_for_highest_speed - altitude_for_lowest_speed) * factor
)
logging.debug(
f"Preferred patrol altitude for {self.dcs_unit_type.id}: {altitude.feet}"
)
rounded_altitude = feet(round(1000 * round(altitude.feet / 1000)))
return max(
altitude_for_lowest_speed,
min(altitude_for_highest_speed, rounded_altitude),
)
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
from gen.radios import ChannelInUseError, MHz
from gen.radios import ChannelInUseError, kHz
if self.intra_flight_radio is not None:
return radio_registry.alloc_for_radio(self.intra_flight_radio)
freq = MHz(self.dcs_unit_type.radio_frequency)
# The default radio frequency is set in megahertz. For some aircraft, it is a
# floating point value. For all current aircraft, adjusting to kilohertz will be
# sufficient to convert to an integer.
in_khz = float(self.dcs_unit_type.radio_frequency) * 1000
if not in_khz.is_integer():
logging.warning(
f"Found unexpected sub-kHz default radio for {self}: {in_khz} kHz. "
"Truncating to integer. The truncated frequency may not be valid for "
"the aircraft."
)
freq = kHz(int(in_khz))
try:
radio_registry.reserve(freq)
except ChannelInUseError:
@@ -222,6 +298,25 @@ class AircraftType(UnitType[FlyingType]):
radio_config = RadioConfig.from_data(data.get("radios", {}))
patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
try:
mission_range = nautical_miles(int(data["max_range"]))
except (KeyError, ValueError):
mission_range = (
nautical_miles(50) if aircraft.helicopter else nautical_miles(150)
)
logging.warning(
f"{aircraft.id} does not specify a max_range. Defaulting to "
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:
introduction = data["introduced"]
if introduction is None:
@@ -233,7 +328,10 @@ class AircraftType(UnitType[FlyingType]):
yield AircraftType(
dcs_unit_type=aircraft,
name=variant,
description=data.get("description", "No data."),
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
@@ -246,6 +344,8 @@ class AircraftType(UnitType[FlyingType]):
max_group_size=data.get("max_group_size", aircraft.group_size_max),
patrol_altitude=patrol_config.altitude,
patrol_speed=patrol_config.speed,
max_mission_range=mission_range,
fuel_consumption=fuel_consumption,
intra_flight_radio=radio_config.intra_flight,
channel_allocator=radio_config.channel_allocator,
channel_namer=radio_config.channel_namer,

View File

@@ -15,7 +15,7 @@ from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class GroundUnitType(UnitType[VehicleType]):
class GroundUnitType(UnitType[Type[VehicleType]]):
unit_class: Optional[GroundUnitClass]
spawn_weight: int
@@ -88,7 +88,10 @@ class GroundUnitType(UnitType[VehicleType]):
unit_class=unit_class,
spawn_weight=data.get("spawn_weight", 0),
name=variant,
description=data.get("description", "No data."),
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),

View File

@@ -4,12 +4,12 @@ from typing import TypeVar, Generic, Type
from dcs.unittype import UnitType as DcsUnitType
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=DcsUnitType)
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
@dataclass(frozen=True)
class UnitType(Generic[DcsUnitTypeT]):
dcs_unit_type: Type[DcsUnitTypeT]
dcs_unit_type: DcsUnitTypeT
name: str
description: str
year_introduced: str