mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'develop' into helipads
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
from .aircraft import *
|
||||
from .armor import *
|
||||
from .airsupportgen import *
|
||||
from .conflictgen import *
|
||||
from .visualgen import *
|
||||
from .triggergen import *
|
||||
from .environmentgen import *
|
||||
from .groundobjectsgen import *
|
||||
from .briefinggen import *
|
||||
from .forcedoptionsgen import *
|
||||
from .kneeboard import *
|
||||
|
||||
from . import naming
|
||||
|
||||
@@ -69,7 +69,6 @@ from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.factions.faction import Faction
|
||||
from game.settings import Settings
|
||||
from game.squadrons import Pilot
|
||||
from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@@ -109,6 +108,7 @@ from .naming import namegen
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.squadrons import Pilot, Squadron
|
||||
|
||||
WARM_START_HELI_ALT = meters(500)
|
||||
WARM_START_ALTITUDE = meters(3000)
|
||||
@@ -644,8 +644,7 @@ class AircraftConflictGenerator:
|
||||
def spawn_unused_aircraft(
|
||||
self, player_country: Country, enemy_country: Country
|
||||
) -> None:
|
||||
inventories = self.game.aircraft_inventory.inventories
|
||||
for control_point, inventory in inventories.items():
|
||||
for control_point in self.game.theater.controlpoints:
|
||||
if not isinstance(control_point, Airfield):
|
||||
continue
|
||||
|
||||
@@ -655,11 +654,9 @@ class AircraftConflictGenerator:
|
||||
else:
|
||||
country = enemy_country
|
||||
|
||||
for aircraft, available in inventory.all_aircraft:
|
||||
for squadron in control_point.squadrons:
|
||||
try:
|
||||
self._spawn_unused_at(
|
||||
control_point, country, faction, aircraft, available
|
||||
)
|
||||
self._spawn_unused_at(control_point, country, faction, squadron)
|
||||
except NoParkingSlotError:
|
||||
# If we run out of parking, stop spawning aircraft.
|
||||
return
|
||||
@@ -669,17 +666,16 @@ class AircraftConflictGenerator:
|
||||
control_point: Airfield,
|
||||
country: Country,
|
||||
faction: Faction,
|
||||
aircraft: AircraftType,
|
||||
number: int,
|
||||
squadron: Squadron,
|
||||
) -> None:
|
||||
for _ in range(number):
|
||||
for _ in range(squadron.untasked_aircraft):
|
||||
# Creating a flight even those this isn't a fragged mission lets us
|
||||
# reuse the existing debriefing code.
|
||||
# TODO: Special flight type?
|
||||
flight = Flight(
|
||||
Package(control_point),
|
||||
faction.country,
|
||||
self.game.air_wing_for(control_point.captured).squadron_for(aircraft),
|
||||
squadron,
|
||||
1,
|
||||
FlightType.BARCAP,
|
||||
"Cold",
|
||||
@@ -691,16 +687,13 @@ class AircraftConflictGenerator:
|
||||
group = self._generate_at_airport(
|
||||
name=namegen.next_aircraft_name(country, control_point.id, flight),
|
||||
side=country,
|
||||
unit_type=aircraft.dcs_unit_type,
|
||||
unit_type=squadron.aircraft.dcs_unit_type,
|
||||
count=1,
|
||||
start_type="Cold",
|
||||
airport=control_point.airport,
|
||||
)
|
||||
|
||||
if aircraft in faction.liveries_overrides:
|
||||
livery = random.choice(faction.liveries_overrides[aircraft])
|
||||
for unit in group.units:
|
||||
unit.livery_id = livery
|
||||
self._setup_livery(flight, group)
|
||||
|
||||
group.uncontrolled = True
|
||||
self.unit_map.add_aircraft(group, flight)
|
||||
@@ -1837,17 +1830,11 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Set orbit speeds for all race tracks and remove this special case.
|
||||
if isinstance(flight_plan, RefuelingFlightPlan):
|
||||
orbit = OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack,
|
||||
speed=int(flight_plan.patrol_speed.kph),
|
||||
)
|
||||
else:
|
||||
orbit = OrbitAction(
|
||||
altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
)
|
||||
orbit = OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack,
|
||||
speed=int(flight_plan.patrol_speed.kph),
|
||||
)
|
||||
|
||||
racetrack = ControlledTask(orbit)
|
||||
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
||||
|
||||
@@ -5,7 +5,8 @@ from datetime import timedelta
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen import RadioFrequency, TacanChannel
|
||||
from gen.radios import RadioFrequency
|
||||
from gen.tacan import TacanChannel
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -16,8 +16,7 @@ from dcs.task import (
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game.utils import Heading
|
||||
from . import AirSupport
|
||||
from .airsupport import TankerInfo, AwacsInfo
|
||||
from .airsupport import AirSupport, TankerInfo, AwacsInfo
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
from .flights.ai_flight_planner_db import AEWC_CAPABLE
|
||||
|
||||
@@ -129,7 +129,6 @@ CAP_CAPABLE = [
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
Su_33,
|
||||
Su_34,
|
||||
J_11A,
|
||||
Su_30,
|
||||
Su_27,
|
||||
|
||||
@@ -2,20 +2,19 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import List, Optional, TYPE_CHECKING, Union, Sequence, Any
|
||||
from typing import List, Optional, TYPE_CHECKING, Union, Sequence
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unit import Unit
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.savecompat import has_save_compat_for
|
||||
from game.squadrons import Pilot, Squadron
|
||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||
from game.utils import Distance, meters
|
||||
from gen.flights.loadouts import Loadout
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.squadrons import Pilot, Squadron
|
||||
from game.transfers import TransferOrder
|
||||
from gen.ato import Package
|
||||
from gen.flights.flightplan import FlightPlan
|
||||
@@ -50,6 +49,8 @@ class FlightType(Enum):
|
||||
strike-like missions will need more specialized control.
|
||||
* ai_flight_planner.py: Use the new mission type in propose_missions so the AI will
|
||||
plan the new mission type.
|
||||
* FlightType.is_air_to_air and FlightType.is_air_to_ground: If the new mission type
|
||||
fits either of these categories, update those methods accordingly.
|
||||
"""
|
||||
|
||||
TARCAP = "TARCAP"
|
||||
@@ -80,6 +81,30 @@ class FlightType(Enum):
|
||||
return entry
|
||||
raise KeyError(f"No FlightType with name {name}")
|
||||
|
||||
@property
|
||||
def is_air_to_air(self) -> bool:
|
||||
return self in {
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.INTERCEPTION,
|
||||
FlightType.ESCORT,
|
||||
FlightType.SWEEP,
|
||||
}
|
||||
|
||||
@property
|
||||
def is_air_to_ground(self) -> bool:
|
||||
return self in {
|
||||
FlightType.CAS,
|
||||
FlightType.STRIKE,
|
||||
FlightType.ANTISHIP,
|
||||
FlightType.SEAD,
|
||||
FlightType.DEAD,
|
||||
FlightType.BAI,
|
||||
FlightType.OCA_RUNWAY,
|
||||
FlightType.OCA_AIRCRAFT,
|
||||
FlightType.SEAD_ESCORT,
|
||||
}
|
||||
|
||||
|
||||
class FlightWaypointType(Enum):
|
||||
"""Enumeration of waypoint types.
|
||||
@@ -141,8 +166,8 @@ class FlightWaypoint:
|
||||
waypoint_type: The waypoint type.
|
||||
x: X coordinate of the waypoint.
|
||||
y: Y coordinate of the waypoint.
|
||||
alt: Altitude of the waypoint. By default this is AGL, but it can be
|
||||
changed to MSL by setting alt_type to "RADIO".
|
||||
alt: Altitude of the waypoint. By default this is MSL, but it can be
|
||||
changed to AGL by setting alt_type to "RADIO"
|
||||
"""
|
||||
self.waypoint_type = waypoint_type
|
||||
self.x = x
|
||||
@@ -169,12 +194,6 @@ class FlightWaypoint:
|
||||
self.tot: 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
|
||||
def position(self) -> Point:
|
||||
return Point(self.x, self.y)
|
||||
@@ -273,6 +292,7 @@ class Flight:
|
||||
self.package = package
|
||||
self.country = country
|
||||
self.squadron = squadron
|
||||
self.squadron.claim_inventory(count)
|
||||
if roster is None:
|
||||
self.roster = FlightRoster(self.squadron, initial_size=count)
|
||||
else:
|
||||
@@ -321,6 +341,7 @@ class Flight:
|
||||
return self.flight_plan.waypoints[1:]
|
||||
|
||||
def resize(self, new_size: int) -> None:
|
||||
self.squadron.claim_inventory(new_size - self.count)
|
||||
self.roster.resize(new_size)
|
||||
|
||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
||||
@@ -330,8 +351,9 @@ class Flight:
|
||||
def missing_pilots(self) -> int:
|
||||
return self.roster.missing_pilots
|
||||
|
||||
def clear_roster(self) -> None:
|
||||
def return_pilots_and_aircraft(self) -> None:
|
||||
self.roster.clear()
|
||||
self.squadron.claim_inventory(-self.count)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.custom_name:
|
||||
|
||||
@@ -411,6 +411,9 @@ class PatrollingFlightPlan(FlightPlan):
|
||||
#: Maximum time to remain on station.
|
||||
patrol_duration: timedelta
|
||||
|
||||
#: Racetrack speed TAS.
|
||||
patrol_speed: Speed
|
||||
|
||||
#: The engagement range of any Search Then Engage task, or the radius of a
|
||||
#: Search Then Engage in Zone task. Any enemies of the appropriate type for
|
||||
#: this mission within this range of the flight's current position (or the
|
||||
@@ -779,9 +782,6 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
|
||||
divert: Optional[FlightWaypoint]
|
||||
bullseye: FlightWaypoint
|
||||
|
||||
#: Racetrack speed.
|
||||
patrol_speed: Speed
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
@@ -1115,7 +1115,7 @@ class FlightPlanBuilder:
|
||||
if isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
start_pos, end_pos = self.racetrack_for_objective(location, barcap=True)
|
||||
start_pos, end_pos = self.cap_racetrack_for_objective(location, barcap=True)
|
||||
|
||||
preferred_alt = flight.unit_type.preferred_patrol_altitude
|
||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||
@@ -1124,6 +1124,11 @@ class FlightPlanBuilder:
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
|
||||
patrol_speed = flight.unit_type.preferred_patrol_speed(patrol_alt)
|
||||
logging.debug(
|
||||
f"BARCAP patrol speed for {flight.unit_type.name} at {patrol_alt.feet}ft: {patrol_speed.knots} KTAS"
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
|
||||
|
||||
@@ -1131,6 +1136,7 @@ class FlightPlanBuilder:
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
patrol_duration=self.doctrine.cap_duration,
|
||||
patrol_speed=patrol_speed,
|
||||
engagement_distance=self.doctrine.cap_engagement_range,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
@@ -1238,7 +1244,7 @@ class FlightPlanBuilder:
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
def racetrack_for_objective(
|
||||
def cap_racetrack_for_objective(
|
||||
self, location: MissionTarget, barcap: bool
|
||||
) -> Tuple[Point, Point]:
|
||||
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
@@ -1270,6 +1276,7 @@ class FlightPlanBuilder:
|
||||
- self.doctrine.cap_engagement_range
|
||||
- nautical_miles(5)
|
||||
)
|
||||
max_track_length = self.doctrine.cap_max_track_length
|
||||
else:
|
||||
# Other race tracks (TARCAPs, currently) just try to keep some
|
||||
# distance from the nearest enemy airbase, but since they are by
|
||||
@@ -1283,6 +1290,11 @@ class FlightPlanBuilder:
|
||||
)
|
||||
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
||||
|
||||
# TARCAPs fly short racetracks because they need to react faster.
|
||||
max_track_length = self.doctrine.cap_min_track_length + 0.3 * (
|
||||
self.doctrine.cap_max_track_length - self.doctrine.cap_min_track_length
|
||||
)
|
||||
|
||||
min_cap_distance = min(
|
||||
self.doctrine.cap_min_distance_from_cp, distance_to_no_fly
|
||||
)
|
||||
@@ -1294,11 +1306,12 @@ class FlightPlanBuilder:
|
||||
heading.degrees,
|
||||
random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)),
|
||||
)
|
||||
diameter = random.randint(
|
||||
|
||||
track_length = random.randint(
|
||||
int(self.doctrine.cap_min_track_length.meters),
|
||||
int(self.doctrine.cap_max_track_length.meters),
|
||||
int(max_track_length.meters),
|
||||
)
|
||||
start = end.point_from_heading(heading.opposite.degrees, diameter)
|
||||
start = end.point_from_heading(heading.opposite.degrees, track_length)
|
||||
return start, end
|
||||
|
||||
def aewc_orbit(self, location: MissionTarget) -> Point:
|
||||
@@ -1321,33 +1334,6 @@ class FlightPlanBuilder:
|
||||
orbit_heading.degrees, orbit_distance.meters
|
||||
)
|
||||
|
||||
def racetrack_for_frontline(
|
||||
self, origin: Point, front_line: FrontLine
|
||||
) -> Tuple[Point, Point]:
|
||||
# Find targets waypoints
|
||||
ingress, heading, distance = Conflict.frontline_vector(front_line, self.theater)
|
||||
center = ingress.point_from_heading(heading.degrees, distance / 2)
|
||||
orbit_center = center.point_from_heading(
|
||||
heading.left.degrees,
|
||||
random.randint(
|
||||
int(nautical_miles(6).meters), int(nautical_miles(15).meters)
|
||||
),
|
||||
)
|
||||
|
||||
combat_width = distance / 2
|
||||
if combat_width > 500000:
|
||||
combat_width = 500000
|
||||
if combat_width < 35000:
|
||||
combat_width = 35000
|
||||
|
||||
radius = combat_width * 1.25
|
||||
start = orbit_center.point_from_heading(heading.degrees, radius)
|
||||
end = orbit_center.point_from_heading(heading.opposite.degrees, radius)
|
||||
|
||||
if end.distance_to_point(origin) < start.distance_to_point(origin):
|
||||
start, end = end, start
|
||||
return start, end
|
||||
|
||||
def generate_tarcap(self, flight: Flight) -> TarCapFlightPlan:
|
||||
"""Generate a CAP flight plan for the given front line.
|
||||
|
||||
@@ -1362,16 +1348,14 @@ class FlightPlanBuilder:
|
||||
self.doctrine.min_patrol_altitude,
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
patrol_speed = flight.unit_type.preferred_patrol_speed(patrol_alt)
|
||||
logging.debug(
|
||||
f"TARCAP patrol speed for {flight.unit_type.name} at {patrol_alt.feet}ft: {patrol_speed.knots} KTAS"
|
||||
)
|
||||
|
||||
# Create points
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
|
||||
if isinstance(location, FrontLine):
|
||||
orbit0p, orbit1p = self.racetrack_for_frontline(
|
||||
flight.departure.position, location
|
||||
)
|
||||
else:
|
||||
orbit0p, orbit1p = self.racetrack_for_objective(location, barcap=False)
|
||||
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
|
||||
|
||||
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||
return TarCapFlightPlan(
|
||||
@@ -1383,6 +1367,7 @@ class FlightPlanBuilder:
|
||||
# requests an escort the CAP flight will remain on station for the
|
||||
# duration of the escorted mission, or until it is winchester/bingo.
|
||||
patrol_duration=self.doctrine.cap_duration,
|
||||
patrol_speed=patrol_speed,
|
||||
engagement_distance=self.doctrine.cap_engagement_range,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(flight.departure.position, orbit0p, patrol_alt),
|
||||
@@ -1546,16 +1531,33 @@ class FlightPlanBuilder:
|
||||
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
|
||||
# 2021-08-02: patrol_speed will currently have no effect because
|
||||
# CAS doesn't use OrbitAction. But all PatrollingFlightPlan are expected
|
||||
# to have patrol_speed
|
||||
is_helo = flight.unit_type.dcs_unit_type.helicopter
|
||||
ingress_egress_altitude = (
|
||||
self.doctrine.ingress_altitude if not is_helo else meters(50)
|
||||
)
|
||||
patrol_speed = flight.unit_type.preferred_patrol_speed(ingress_egress_altitude)
|
||||
use_agl_ingress_egress = is_helo
|
||||
|
||||
return CasFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
patrol_duration=self.doctrine.cas_duration,
|
||||
patrol_speed=patrol_speed,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
flight.departure.position, ingress, self.doctrine.ingress_altitude
|
||||
flight.departure.position,
|
||||
ingress,
|
||||
ingress_egress_altitude,
|
||||
use_agl_ingress_egress,
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
egress, flight.arrival.position, self.doctrine.ingress_altitude
|
||||
egress,
|
||||
flight.arrival.position,
|
||||
ingress_egress_altitude,
|
||||
use_agl_ingress_egress,
|
||||
),
|
||||
patrol_start=builder.ingress(
|
||||
FlightWaypointType.INGRESS_CAS, ingress, location
|
||||
@@ -1608,6 +1610,7 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
altitude = feet(21000)
|
||||
|
||||
# TODO: Could use flight.unit_type.preferred_patrol_speed(altitude) instead.
|
||||
if tanker_type.patrol_speed is not None:
|
||||
speed = tanker_type.patrol_speed
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
@@ -55,7 +56,7 @@ class WaypointBuilder:
|
||||
|
||||
@property
|
||||
def is_helo(self) -> bool:
|
||||
return getattr(self.flight.unit_type, "helicopter", False)
|
||||
return self.flight.unit_type.dcs_unit_type.helicopter
|
||||
|
||||
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
|
||||
"""Create takeoff waypoint for the given arrival airfield or carrier.
|
||||
@@ -167,6 +168,8 @@ class WaypointBuilder:
|
||||
position.y,
|
||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||
)
|
||||
if self.is_helo:
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.pretty_name = "Hold"
|
||||
waypoint.description = "Wait until push time"
|
||||
waypoint.name = "HOLD"
|
||||
@@ -210,7 +213,7 @@ class WaypointBuilder:
|
||||
ingress_type,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
)
|
||||
if self.is_helo:
|
||||
waypoint.alt_type = "RADIO"
|
||||
@@ -225,7 +228,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
)
|
||||
if self.is_helo:
|
||||
waypoint.alt_type = "RADIO"
|
||||
@@ -309,7 +312,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.CAS,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(50) if self.is_helo else meters(1000),
|
||||
meters(60) if self.is_helo else meters(1000),
|
||||
)
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Provide CAS"
|
||||
@@ -445,7 +448,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
target.position.x,
|
||||
target.position.y,
|
||||
meters(50) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
)
|
||||
if self.is_helo:
|
||||
waypoint.alt_type = "RADIO"
|
||||
|
||||
@@ -43,8 +43,14 @@ class ForcedOptionsGenerator:
|
||||
if blue.unrestricted_satnav or red.unrestricted_satnav:
|
||||
self.mission.forced_options.unrestricted_satnav = True
|
||||
|
||||
def _set_battle_damage_assessment(self) -> None:
|
||||
self.mission.forced_options.battle_damage_assessment = (
|
||||
self.game.settings.battle_damage_assessment
|
||||
)
|
||||
|
||||
def generate(self) -> None:
|
||||
self._set_options_view()
|
||||
self._set_external_views()
|
||||
self._set_labels()
|
||||
self._set_unrestricted_satnav()
|
||||
self._set_battle_damage_assessment()
|
||||
|
||||
@@ -65,7 +65,8 @@ class KneeboardPageWriter:
|
||||
else:
|
||||
self.foreground_fill = (15, 15, 15)
|
||||
self.background_fill = (255, 252, 252)
|
||||
self.image = Image.new("RGB", (768, 1024), self.background_fill)
|
||||
self.image_size = (768, 1024)
|
||||
self.image = Image.new("RGB", self.image_size, self.background_fill)
|
||||
# These font sizes create a relatively full page for current sorties. If
|
||||
# we start generating more complicated flight plans, or start including
|
||||
# more information in the comm ladder (the latter of which we should
|
||||
@@ -84,6 +85,7 @@ class KneeboardPageWriter:
|
||||
"resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC
|
||||
)
|
||||
self.draw = ImageDraw.Draw(self.image)
|
||||
self.page_margin = page_margin
|
||||
self.x = page_margin
|
||||
self.y = page_margin
|
||||
self.line_spacing = line_spacing
|
||||
@@ -97,12 +99,21 @@ class KneeboardPageWriter:
|
||||
text: str,
|
||||
font: Optional[ImageFont.FreeTypeFont] = None,
|
||||
fill: Optional[Tuple[int, int, int]] = None,
|
||||
wrap: bool = False,
|
||||
) -> None:
|
||||
if font is None:
|
||||
font = self.content_font
|
||||
if fill is None:
|
||||
fill = self.foreground_fill
|
||||
|
||||
if wrap:
|
||||
text = "\n".join(
|
||||
self.wrap_line_with_font(
|
||||
line, self.image_size[0] - self.page_margin - self.x, font
|
||||
)
|
||||
for line in text.splitlines()
|
||||
)
|
||||
|
||||
self.draw.text(self.position, text, font=font, fill=fill)
|
||||
width, height = self.draw.textsize(text, font=font)
|
||||
self.y += height + self.line_spacing
|
||||
@@ -146,6 +157,24 @@ class KneeboardPageWriter:
|
||||
output = combo
|
||||
return "".join(segments + [output]).strip()
|
||||
|
||||
@staticmethod
|
||||
def wrap_line_with_font(
|
||||
inputstr: str, max_width: int, font: ImageFont.FreeTypeFont
|
||||
) -> str:
|
||||
if font.getsize(inputstr)[0] <= max_width:
|
||||
return inputstr
|
||||
tokens = inputstr.split(" ")
|
||||
output = ""
|
||||
segments = []
|
||||
for token in tokens:
|
||||
combo = output + " " + token
|
||||
if font.getsize(combo)[0] > max_width:
|
||||
segments.append(output + "\n")
|
||||
output = token
|
||||
else:
|
||||
output = combo
|
||||
return "".join(segments + [output]).strip()
|
||||
|
||||
|
||||
class KneeboardPage:
|
||||
"""Base class for all kneeboard pages."""
|
||||
@@ -631,7 +660,7 @@ class NotesPage(KneeboardPage):
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
writer.title(f"Notes")
|
||||
writer.text(self.notes)
|
||||
writer.text(self.notes, wrap=True)
|
||||
writer.write(path)
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import time
|
||||
from typing import List, Any
|
||||
from typing import List, Any, TYPE_CHECKING
|
||||
|
||||
from dcs.country import Country
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.unittype import UnitType
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
ALPHA_MILITARY = [
|
||||
"Alpha",
|
||||
|
||||
115
gen/radios.py
115
gen/radios.py
@@ -2,7 +2,7 @@
|
||||
import itertools
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterator, List, Set
|
||||
from typing import Dict, FrozenSet, Iterator, List, Reversible, Set, Tuple
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -45,14 +45,8 @@ def kHz(num: int) -> RadioFrequency:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Radio:
|
||||
"""A radio.
|
||||
|
||||
Defines the minimum (inclusive) and maximum (exclusive) range of the radio.
|
||||
"""
|
||||
|
||||
#: The name of the radio.
|
||||
name: str
|
||||
class RadioRange:
|
||||
"""Defines the minimum (inclusive) and maximum (exclusive) range of the radio."""
|
||||
|
||||
#: The minimum (inclusive) frequency tunable by this radio.
|
||||
minimum: RadioFrequency
|
||||
@@ -63,19 +57,51 @@ class Radio:
|
||||
#: The spacing between adjacent frequencies.
|
||||
step: RadioFrequency
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
#: Specific frequencies to exclude. (e.g. Guard channels)
|
||||
excludes: FrozenSet[RadioFrequency] = frozenset()
|
||||
|
||||
def range(self) -> Iterator[RadioFrequency]:
|
||||
"""Returns an iterator over the usable frequencies of this radio."""
|
||||
return (
|
||||
RadioFrequency(x)
|
||||
for x in range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
|
||||
if RadioFrequency(x) not in self.excludes
|
||||
)
|
||||
|
||||
@property
|
||||
def last_channel(self) -> RadioFrequency:
|
||||
return RadioFrequency(self.maximum.hertz - self.step.hertz)
|
||||
return next(
|
||||
RadioFrequency(x)
|
||||
for x in reversed(
|
||||
range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
|
||||
)
|
||||
if RadioFrequency(x) not in self.excludes
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Radio:
|
||||
"""A radio.
|
||||
|
||||
Defines ranges of usable frequencies of the radio.
|
||||
"""
|
||||
|
||||
#: The name of the radio.
|
||||
name: str
|
||||
|
||||
#: List of usable frequency range of this radio.
|
||||
ranges: Tuple[RadioRange, ...]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def range(self) -> Iterator[RadioFrequency]:
|
||||
"""Returns an iterator over the usable frequencies of this radio."""
|
||||
return itertools.chain.from_iterable(rng.range() for rng in self.ranges)
|
||||
|
||||
@property
|
||||
def last_channel(self) -> RadioFrequency:
|
||||
return self.ranges[-1].last_channel
|
||||
|
||||
|
||||
class ChannelInUseError(RuntimeError):
|
||||
@@ -88,53 +114,58 @@ class ChannelInUseError(RuntimeError):
|
||||
# TODO: Figure out appropriate steps for each radio. These are just guesses.
|
||||
#: List of all known radios used by aircraft in the game.
|
||||
RADIOS: List[Radio] = [
|
||||
Radio("AN/ARC-164", MHz(225), MHz(400), step=MHz(1)),
|
||||
Radio("AN/ARC-186(V) AM", MHz(116), MHz(152), step=MHz(1)),
|
||||
Radio("AN/ARC-186(V) FM", MHz(30), MHz(76), step=MHz(1)),
|
||||
# The AN/ARC-210 can also use [30, 88) and [108, 118), but the current
|
||||
# implementation can't implement the gap and the radio can't transmit on the
|
||||
# latter. There's still plenty of channels between 118 MHz and 400 MHz, so
|
||||
# not worth worrying about.
|
||||
Radio("AN/ARC-210", MHz(118), MHz(400), step=MHz(1)),
|
||||
Radio("AN/ARC-222", MHz(116), MHz(174), step=MHz(1)),
|
||||
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
|
||||
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
|
||||
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
|
||||
Radio("AN/ARC-164", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)),
|
||||
Radio("AN/ARC-186(V) AM", (RadioRange(MHz(116), MHz(152), step=MHz(1)),)),
|
||||
Radio("AN/ARC-186(V) FM", (RadioRange(MHz(30), MHz(76), step=MHz(1)),)),
|
||||
Radio(
|
||||
"AN/ARC-210",
|
||||
(
|
||||
RadioRange(MHz(225), MHz(400), MHz(1), frozenset((MHz(243),))),
|
||||
RadioRange(MHz(136), MHz(155), MHz(1)),
|
||||
RadioRange(MHz(156), MHz(174), MHz(1)),
|
||||
RadioRange(MHz(118), MHz(136), MHz(1)),
|
||||
RadioRange(MHz(30), MHz(88), MHz(1)),
|
||||
),
|
||||
),
|
||||
Radio("AN/ARC-222", (RadioRange(MHz(116), MHz(174), step=MHz(1)),)),
|
||||
Radio("SCR-522", (RadioRange(MHz(100), MHz(156), step=MHz(1)),)),
|
||||
Radio("A.R.I. 1063", (RadioRange(MHz(100), MHz(156), step=MHz(1)),)),
|
||||
Radio("BC-1206", (RadioRange(kHz(200), kHz(400), step=kHz(10)),)),
|
||||
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between
|
||||
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current
|
||||
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
|
||||
# can model gaps later if needed.
|
||||
Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)),
|
||||
Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)),
|
||||
Radio("TRT ERA 7000 V/UHF", (RadioRange(MHz(118), MHz(150), step=MHz(1)),)),
|
||||
Radio("TRT ERA 7200 UHF", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)),
|
||||
# Tomcat radios
|
||||
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
|
||||
Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)),
|
||||
Radio("AN/ARC-159", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)),
|
||||
# AN/ARC-182 can also operate from 30 MHz to 88 MHz, as well as from 225 MHz
|
||||
# to 400 MHz range, but we can't model gaps with the current implementation.
|
||||
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
|
||||
Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)),
|
||||
Radio("AN/ARC-182", (RadioRange(MHz(108), MHz(174), step=MHz(1)),)),
|
||||
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps.
|
||||
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
|
||||
Radio("FR 22", (RadioRange(MHz(225), MHz(400), step=kHz(50)),)),
|
||||
# P-51 / P-47 Radio
|
||||
# 4 preset channels (A/B/C/D)
|
||||
Radio("SCR522", MHz(100), MHz(156), step=kHz(25)),
|
||||
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
|
||||
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
|
||||
Radio("SCR522", (RadioRange(MHz(100), MHz(156), step=kHz(25)),)),
|
||||
Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), step=MHz(1)),)),
|
||||
Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), step=MHz(1)),)),
|
||||
# MiG-15bis
|
||||
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
|
||||
Radio("RSI-6K HF", (RadioRange(MHz(3, 750), MHz(5), step=kHz(25)),)),
|
||||
# MiG-19P
|
||||
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
|
||||
Radio("RSIU-4V", (RadioRange(MHz(100), MHz(150), step=MHz(1)),)),
|
||||
# MiG-21bis
|
||||
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
|
||||
Radio("RSIU-5V", (RadioRange(MHz(118), MHz(140), step=MHz(1)),)),
|
||||
# Ka-50
|
||||
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
|
||||
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
|
||||
Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
|
||||
Radio("R-800L1", (RadioRange(MHz(220), MHz(400), step=kHz(25)),)),
|
||||
Radio("R-828", (RadioRange(MHz(20), MHz(60), step=kHz(25)),)),
|
||||
# UH-1H
|
||||
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
|
||||
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
|
||||
Radio("AN/ARC-134", MHz(116), MHz(150), step=kHz(25)),
|
||||
Radio("R&S Series 6000", MHz(100), MHz(156), step=kHz(25)),
|
||||
Radio("AN/ARC-51BX", (RadioRange(MHz(225), MHz(400), step=kHz(50)),)),
|
||||
Radio("AN/ARC-131", (RadioRange(MHz(30), MHz(76), step=kHz(50)),)),
|
||||
Radio("AN/ARC-134", (RadioRange(MHz(116), MHz(150), step=kHz(25)),)),
|
||||
Radio("R&S Series 6000", (RadioRange(MHz(100), MHz(156), step=kHz(25)),)),
|
||||
]
|
||||
|
||||
|
||||
@@ -175,7 +206,7 @@ class RadioRegistry:
|
||||
|
||||
# Not a real radio, but useful for allocating a channel usable for
|
||||
# inter-flight communications.
|
||||
BLUFOR_UHF = Radio("BLUFOR UHF", MHz(225), MHz(400), step=MHz(1))
|
||||
BLUFOR_UHF = Radio("BLUFOR UHF", (RadioRange(MHz(225), MHz(400), step=MHz(1)),))
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.allocated_channels: Set[RadioFrequency] = set()
|
||||
|
||||
@@ -82,8 +82,10 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
||||
)
|
||||
|
||||
# Some Opel Blitz trucks
|
||||
index = 0
|
||||
for i in range(int(max(1, 2))):
|
||||
for j in range(int(max(1, 2))):
|
||||
index += 1
|
||||
self.add_unit(
|
||||
Unarmed.Blitz_36_6700A,
|
||||
"BLITZ#" + str(index),
|
||||
|
||||
@@ -16,7 +16,11 @@ class EwrGenerator(VehicleGroupGenerator[EwrGroundObject]):
|
||||
|
||||
def generate(self) -> None:
|
||||
self.add_unit(
|
||||
self.unit_type, "EWR", self.position.x, self.position.y, self.heading
|
||||
self.unit_type,
|
||||
"EWR",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading_to_conflict(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user