Merge remote-tracking branch 'khopa/develop_2_3_x' into develop_2_3_x

This commit is contained in:
Khopa 2020-12-19 23:54:28 +01:00
commit 02ecfebb85
12 changed files with 433 additions and 358 deletions

View File

@ -4,11 +4,15 @@
* **[Units]** Support for newly added BTR-82A, T-72B3
* **[Units]** Added ZSU-57 AAA sites
* **[Culling]** BARCAP missions no longer create culling exclusion zones.
* **[Flight Planner]** Improved TOT planning. Negative start times no longer occur with TARCAPs and hold times no longer affect planning for flight plans without hold points.
* **[Factions]** Added Iraq 1991 faction (thanks again to Hawkmoon!)
## Fixes:
* **[Mission Generator]** Fix mission generation error when there are too many radio frequency to setup for the Mig-21
* **[Mission Generator]** Fix ground units not moving forward
* **[Mission Generator]** Fixed assigned radio channels overlapping with beacons.
* **[Flight Planner]** Fix creation of custom waypoints.
* **[Campaigns]** Fixed many cases of SAMs spawning on the runways/taxiways in Syria Full.
# 2.3.1

View File

@ -199,10 +199,14 @@ class Operation:
@classmethod
def create_radio_registries(cls) -> None:
unique_map_frequencies = set() # type: Set[RadioFrequency]
unique_map_frequencies: Set[RadioFrequency] = set()
cls._create_tacan_registry(unique_map_frequencies)
cls._create_radio_registry(unique_map_frequencies)
assert cls.radio_registry is not None
for frequency in unique_map_frequencies:
cls.radio_registry.reserve(frequency)
@classmethod
def assign_channels_to_flights(cls, flights: List[FlightData],
air_support: AirSupport) -> None:
@ -256,8 +260,8 @@ class Operation:
unique_map_frequencies.add(data.atc.vhf_fm)
unique_map_frequencies.add(data.atc.vhf_am)
unique_map_frequencies.add(data.atc.uhf)
# No need to reserve ILS or TACAN because those are in the
# beacon list.
# No need to reserve ILS or TACAN because those are in the
# beacon list.
@classmethod
def _generate_ground_units(cls):

View File

@ -2,7 +2,7 @@ from pathlib import Path
def _build_version_string() -> str:
components = ["2.3.2"]
components = ["2.3.3"]
build_number_path = Path("resources/buildnumber")
if build_number_path.exists():
with build_number_path.open("r") as build_number_file:

View File

@ -20,20 +20,20 @@ from dcs.planes import (
B_17G,
B_52H,
Bf_109K_4,
C_101EB,
C_101CC,
C_101EB,
FW_190A8,
FW_190D9,
F_14B,
I_16,
JF_17,
Ju_88A4,
PlaneType,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
PlaneType,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_33,
@ -59,16 +59,15 @@ from dcs.task import (
OptReactOnThreat,
OptRestrictJettison,
OrbitAction,
PinpointStrike,
RunwayAttack,
SEAD,
StartCommand,
Targets,
Task,
WeaponType,
PinpointStrike,
)
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unittype import FlyingType, UnitType
@ -99,7 +98,6 @@ from gen.flights.flight import (
)
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData
from .conflictgen import Conflict
from .flights.flightplan import (
CasFlightPlan,
LoiterFlightPlan,
@ -108,7 +106,6 @@ from .flights.flightplan import (
)
from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen
from .runways import RunwayAssigner
if TYPE_CHECKING:
from game import Game
@ -1349,7 +1346,7 @@ class AircraftConflictGenerator:
# And setting *our* waypoint TOT causes the takeoff time to show up in
# the player's kneeboard.
waypoint.tot = estimator.takeoff_time_for_flight(flight)
waypoint.tot = flight.flight_plan.takeoff_time()
# And finally assign it to the FlightData info so it shows correctly in
# the briefing.
self.flights[-1].departure_delay = start_time

View File

@ -73,10 +73,17 @@ class FlightPlan:
"""Iterates over all waypoints in the flight plan, in order."""
raise NotImplementedError
@property
def edges(self) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
def edges(
self, until: Optional[FlightWaypoint] = None
) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
"""A list of all paths between waypoints, in order."""
return zip(self.waypoints, self.waypoints[1:])
waypoints = self.waypoints
if until is None:
last_index = len(waypoints)
else:
last_index = waypoints.index(until) + 1
return zip(self.waypoints[:last_index], self.waypoints[1:last_index])
def best_speed_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> int:
@ -137,7 +144,6 @@ class FlightPlan:
"""Joker fuel value for the FlightPlan
"""
return self.bingo_fuel + 1000
def max_distance_from(self, cp: ControlPoint) -> int:
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
@ -156,26 +162,18 @@ class FlightPlan:
"""
return timedelta()
# Not cached because changes to the package might alter the formation speed.
@property
def travel_time_to_target(self) -> Optional[timedelta]:
"""The estimated time between the first waypoint and the target."""
if self.tot_waypoint is None:
return None
return self._travel_time_to_waypoint(self.tot_waypoint)
def _travel_time_to_waypoint(
self, destination: FlightWaypoint) -> timedelta:
total = timedelta()
for previous_waypoint, waypoint in self.edges:
total += self.travel_time_between_waypoints(previous_waypoint,
waypoint)
if waypoint == destination:
break
else:
if destination not in self.waypoints:
raise PlanningError(
f"Did not find destination waypoint {destination} in "
f"waypoints for {self.flight}")
for previous_waypoint, waypoint in self.edges(until=destination):
total += self.travel_time_between_waypoints(previous_waypoint,
waypoint)
return total
def travel_time_between_waypoints(self, a: FlightWaypoint,
@ -196,10 +194,59 @@ class FlightPlan:
def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
return None
def takeoff_time(self) -> Optional[timedelta]:
tot_waypoint = self.tot_waypoint
if tot_waypoint is None:
return None
time = self.tot_for_waypoint(tot_waypoint)
if time is None:
return None
time += self.tot_offset
return time - self._travel_time_to_waypoint(tot_waypoint)
def startup_time(self) -> Optional[timedelta]:
takeoff_time = self.takeoff_time()
if takeoff_time is None:
return None
start_time = (takeoff_time - self.estimate_startup() -
self.estimate_ground_ops())
# In case FP math has given us some barely below zero time, round to
# zero.
if math.isclose(start_time.total_seconds(), 0):
return timedelta()
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round down so *barely* above zero start times are just zero.
return timedelta(seconds=math.floor(start_time.total_seconds()))
def estimate_startup(self) -> timedelta:
if self.flight.start_type == "Cold":
if self.flight.client_count:
return timedelta(minutes=10)
else:
# The AI doesn't seem to have a real startup procedure.
return timedelta(minutes=2)
return timedelta()
def estimate_ground_ops(self) -> timedelta:
if self.flight.start_type in ("Runway", "In Flight"):
return timedelta()
if self.flight.from_cp.is_fleet:
return timedelta(minutes=2)
else:
return timedelta(minutes=5)
@dataclass(frozen=True)
class LoiterFlightPlan(FlightPlan):
hold: FlightWaypoint
hold_duration: timedelta
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
raise NotImplementedError
@ -221,6 +268,17 @@ class LoiterFlightPlan(FlightPlan):
return self.push_time
return None
def travel_time_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> timedelta:
travel_time = super().travel_time_between_waypoints(a, b)
if a != self.hold:
return travel_time
try:
return travel_time + self.hold_duration
except AttributeError:
# Save compat for 2.3.
return travel_time + timedelta(minutes=5)
@dataclass(frozen=True)
class FormationFlightPlan(LoiterFlightPlan):
@ -254,7 +312,7 @@ class FormationFlightPlan(LoiterFlightPlan):
all of its formation waypoints.
"""
speeds = []
for previous_waypoint, waypoint in self.edges:
for previous_waypoint, waypoint in self.edges():
if waypoint in self.package_speed_waypoints:
speeds.append(self.best_speed_between_waypoints(
previous_waypoint, waypoint))
@ -486,7 +544,7 @@ class StrikeFlightPlan(FormationFlightPlan):
"""The estimated time between the first waypoint and the target."""
destination = self.tot_waypoint
total = timedelta()
for previous_waypoint, waypoint in self.edges:
for previous_waypoint, waypoint in self.edges():
if waypoint == self.tot_waypoint:
# For anything strike-like the TOT waypoint is the *flight's*
# mission target, but to synchronize with the rest of the
@ -846,6 +904,7 @@ class FlightPlanBuilder:
lead_time=timedelta(minutes=5),
takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)),
hold_duration=timedelta(minutes=5),
sweep_start=start,
sweep_end=end,
land=builder.land(flight.arrival),
@ -1050,6 +1109,7 @@ class FlightPlanBuilder:
flight=flight,
takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)),
hold_duration=timedelta(minutes=5),
join=builder.join(self.package.waypoints.join),
ingress=ingress,
targets=[target],
@ -1196,6 +1256,7 @@ class FlightPlanBuilder:
flight=flight,
takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)),
hold_duration=timedelta(minutes=5),
join=builder.join(self.package.waypoints.join),
ingress=builder.ingress(ingress_type,
self.package.waypoints.ingress, location),

View File

@ -89,68 +89,23 @@ class TravelTime:
# TODO: Most if not all of this should move into FlightPlan.
class TotEstimator:
# An extra five minutes given as wiggle room. Expected to be spent at the
# hold point performing any last minute configuration.
HOLD_TIME = timedelta(minutes=5)
def __init__(self, package: Package) -> None:
self.package = package
def mission_start_time(self, flight: Flight) -> timedelta:
takeoff_time = self.takeoff_time_for_flight(flight)
if takeoff_time is None:
@staticmethod
def mission_start_time(flight: Flight) -> timedelta:
startup_time = flight.flight_plan.startup_time()
if startup_time is None:
# Could not determine takeoff time, probably due to a custom flight
# plan. Start immediately.
return timedelta()
startup_time = self.estimate_startup(flight)
ground_ops_time = self.estimate_ground_ops(flight)
start_time = takeoff_time - startup_time - ground_ops_time
# In case FP math has given us some barely below zero time, round to
# zero.
if math.isclose(start_time.total_seconds(), 0):
return timedelta()
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round down so *barely* above zero start times are just zero.
return timedelta(seconds=math.floor(start_time.total_seconds()))
def takeoff_time_for_flight(self, flight: Flight) -> Optional[timedelta]:
travel_time = self.travel_time_to_rendezvous_or_target(flight)
if travel_time is None:
from gen.flights.flightplan import CustomFlightPlan
if not isinstance(flight.flight_plan, CustomFlightPlan):
logging.warning(
"Found no rendezvous or target point. Cannot estimate "
f"takeoff time takeoff time for {flight}.")
return None
from gen.flights.flightplan import FormationFlightPlan
if isinstance(flight.flight_plan, FormationFlightPlan):
tot = flight.flight_plan.tot_for_waypoint(
flight.flight_plan.join)
if tot is None:
logging.warning(
"Could not determine the TOT of the join point. Takeoff "
f"time for {flight} will be immediate.")
return None
else:
tot_waypoint = flight.flight_plan.tot_waypoint
if tot_waypoint is None:
tot = self.package.time_over_target
else:
tot = flight.flight_plan.tot_for_waypoint(tot_waypoint)
if tot is None:
logging.error(f"TOT waypoint for {flight} has no TOT")
tot = self.package.time_over_target
return tot - travel_time - self.HOLD_TIME
return startup_time
def earliest_tot(self) -> timedelta:
earliest_tot = max((
self.earliest_tot_for_flight(f) for f in self.package.flights
)) + self.HOLD_TIME
))
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
@ -159,7 +114,8 @@ class TotEstimator:
# Round up so we don't get negative start times.
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
def earliest_tot_for_flight(self, flight: Flight) -> timedelta:
@staticmethod
def earliest_tot_for_flight(flight: Flight) -> timedelta:
"""Estimate fastest time from mission start to the target position.
For BARCAP flights, this is time to race track start. This ensures that
@ -175,51 +131,18 @@ class TotEstimator:
The earliest possible TOT for the given flight in seconds. Returns 0
if an ingress point cannot be found.
"""
time_to_target = self.travel_time_to_target(flight)
if time_to_target is None:
# Clear the TOT, calculate the startup time. Negating the result gives
# the earliest possible start time.
orig_tot = flight.package.time_over_target
try:
flight.package.time_over_target = timedelta()
time = flight.flight_plan.startup_time()
finally:
flight.package.time_over_target = orig_tot
if time is None:
logging.warning(f"Cannot estimate TOT for {flight}")
# Return 0 so this flight's travel time does not affect the rest
# of the package.
return timedelta()
# Account for TOT offsets for the flight plan. An offset of -2 minutes
# means the flight's TOT is 2 minutes ahead of the package's so it needs
# an extra two minutes.
offset = -flight.flight_plan.tot_offset
startup = self.estimate_startup(flight)
ground_ops = self.estimate_ground_ops(flight)
return startup + ground_ops + time_to_target + offset
@staticmethod
def estimate_startup(flight: Flight) -> timedelta:
if flight.start_type == "Cold":
if flight.client_count:
return timedelta(minutes=10)
else:
# The AI doesn't seem to have a real startup procedure.
return timedelta(minutes=2)
return timedelta()
@staticmethod
def estimate_ground_ops(flight: Flight) -> timedelta:
if flight.start_type in ("Runway", "In Flight"):
return timedelta()
if flight.from_cp.is_fleet:
return timedelta(minutes=2)
else:
return timedelta(minutes=5)
@staticmethod
def travel_time_to_target(flight: Flight) -> Optional[timedelta]:
if flight.flight_plan is None:
return None
return flight.flight_plan.travel_time_to_target
@staticmethod
def travel_time_to_rendezvous_or_target(
flight: Flight) -> Optional[timedelta]:
if flight.flight_plan is None:
return None
from gen.flights.flightplan import FormationFlightPlan
if isinstance(flight.flight_plan, FormationFlightPlan):
return flight.flight_plan.travel_time_to_rendezvous
return flight.flight_plan.travel_time_to_target
return -time

View File

@ -7,7 +7,6 @@ from PySide2.QtWidgets import QHeaderView, QTableView
from game.utils import meter_to_feet
from gen.ato import Package
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
from gen.flights.traveltime import TotEstimator
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \
QWaypointItem
@ -74,9 +73,9 @@ class QFlightWaypointList(QTableView):
time = timedelta(seconds=int(time.total_seconds()))
return f"{prefix}T+{time}"
def takeoff_text(self, flight: Flight) -> str:
estimator = TotEstimator(self.package)
takeoff_time = estimator.takeoff_time_for_flight(flight)
@staticmethod
def takeoff_text(flight: Flight) -> str:
takeoff_time = flight.flight_plan.takeoff_time()
# Handle custom flight plans where we can't estimate the takeoff time.
if takeoff_time is None:
takeoff_time = timedelta()

File diff suppressed because it is too large Load Diff

View File

@ -52,14 +52,14 @@
"name": "",
"callsign": "IBND",
"beacon_type": 14,
"hertz": 333800000,
"hertz": 109900000,
"channel": null
},
{
"name": "",
"callsign": "IBND",
"beacon_type": 15,
"hertz": 333800000,
"hertz": 109900000,
"channel": null
},
{
@ -77,12 +77,19 @@
"channel": null
},
{
"name": "BandarEJask",
"name": "JASK",
"callsign": "JSK",
"beacon_type": 9,
"hertz": 349000000,
"hertz": 349000,
"channel": null
},
{
"name": "",
"callsign": "JSK",
"beacon_type": 5,
"hertz": null,
"channel": 110
},
{
"name": "BandarLengeh",
"callsign": "LEN",
@ -101,8 +108,8 @@
"name": "",
"callsign": "MMA",
"beacon_type": 15,
"hertz": 111100000,
"channel": 48
"hertz": 109100000,
"channel": 28
},
{
"name": "",
@ -115,28 +122,28 @@
"name": "",
"callsign": "IMA",
"beacon_type": 15,
"hertz": 109100000,
"channel": 28
"hertz": 111100000,
"channel": 48
},
{
"name": "",
"callsign": "RMA",
"beacon_type": 15,
"hertz": 114900000,
"hertz": 108700000,
"channel": 24
},
{
"name": "",
"callsign": "MMA",
"beacon_type": 14,
"hertz": 111100000,
"channel": 48
"hertz": 109100000,
"channel": 28
},
{
"name": "",
"callsign": "RMA",
"beacon_type": 14,
"hertz": 114900000,
"hertz": 108700000,
"channel": 24
},
{
@ -150,8 +157,8 @@
"name": "",
"callsign": "IMA",
"beacon_type": 14,
"hertz": 109100000,
"channel": 28
"hertz": 111100000,
"channel": 48
},
{
"name": "AlDhafra",
@ -332,7 +339,7 @@
"name": "KishIsland",
"callsign": "KIH",
"beacon_type": 9,
"hertz": 201000000,
"hertz": 201000,
"channel": null
},
{
@ -367,14 +374,14 @@
"name": "LavanIsland",
"callsign": "LVA",
"beacon_type": 9,
"hertz": 310000000,
"hertz": 310000,
"channel": 0
},
{
"name": "LiwaAirbase",
"callsign": "\u00c4\u00bc",
"beacon_type": 7,
"hertz": null,
"callsign": "OMLW",
"beacon_type": 6,
"hertz": 117400000,
"channel": 121
},
{
@ -433,13 +440,6 @@
"hertz": 113600000,
"channel": 83
},
{
"name": "SasAlNakheelAirport",
"callsign": "SAS",
"beacon_type": 10,
"hertz": 128925,
"channel": null
},
{
"name": "SasAlNakheel",
"callsign": "SAS",
@ -500,14 +500,14 @@
"name": "",
"callsign": "ISYZ",
"beacon_type": 15,
"hertz": 109900000,
"hertz": 108500000,
"channel": null
},
{
"name": "",
"callsign": "ISYZ",
"beacon_type": 14,
"hertz": 109900000,
"hertz": 108500000,
"channel": null
},
{
@ -556,7 +556,7 @@
"name": "DezfulAirport",
"callsign": "DZF",
"beacon_type": 9,
"hertz": 293000000,
"hertz": 293000,
"channel": null
},
{

View File

@ -0,0 +1,85 @@
{
"country": "Iraq",
"name": "Iraq 1991",
"authors": "Hawkmoon",
"description": "<p>Iraq forces during desert Storm</p>",
"aircrafts": [
"MiG_19P",
"MiG_21Bis",
"MiG_23MLD",
"MiG_25PD",
"Su_17M4",
"Mi_8MT",
"Su-25",
"Su-24M",
"MiG_25PD",
"Tu_22M3",
"L_39C",
"L_39ZA",
"Mi_24V"
],
"awacs": [
"A_50"
],
"tankers": [
"IL_78M"
],
"frontline_units": [
"IFV_BMP_1",
"APC_MTLB",
"MBT_T_55",
"MBT_T_72B",
"APC_BTR_80",
"ARV_BRDM_2",
"SPH_2S1_Gvozdika"
],
"artillery_units": [
"MLRS_BM_21_Grad"
],
"logistics_units": [
"Transport_Ural_375",
"Transport_UAZ_469"
],
"infantry_units": [
"Paratrooper_AKS",
"Infantry_Soldier_Rus",
"Paratrooper_RPG_16",
"SAM_SA_18_Igla_MANPADS"
],
"air_defenses": [
"ColdWarFlakGenerator",
"EarlyColdWarFlakGenerator",
"SA2Generator",
"SA3Generator",
"SA6Generator",
"SA8Generator",
"SA9Generator",
"SA13Generator",
"ZSU23Generator",
"ZU23Generator",
"ZU23UralGenerator"
],
"ewrs": [
"BoxSpringGenerator"
],
"missiles": [
"ScudGenerator"
],
"missiles_group_count": 1,
"aircraft_carrier": [
],
"helicopter_carrier": [
],
"helicopter_carrier_names": [
],
"destroyers": [
],
"cruisers": [
],
"requirements": {},
"carrier_names": [
],
"navy_generators": [
"GrishaGroupGenerator"
]
}

View File

@ -24,6 +24,7 @@ import argparse
from contextlib import contextmanager
import dataclasses
import gettext
import logging
import os
from pathlib import Path
import textwrap
@ -60,6 +61,7 @@ def convert_lua_frequency(raw: Union[float, int]) -> int:
def beacons_from_terrain(dcs_path: Path, path: Path) -> Iterable[Beacon]:
logging.info(f"Loading terrain data from {path}")
# TODO: Fix case-sensitive issues.
# The beacons.lua file differs by case in some terrains. Will need to be
# fixed if the tool is to be run on Linux, but presumably the server
@ -84,13 +86,20 @@ def beacons_from_terrain(dcs_path: Path, path: Path) -> Iterable[Beacon]:
end
"""))
translator = gettext.translation(
"messages", path / "l10n", languages=["en"])
def translate(message_name: str) -> str:
if not message_name:
try:
translator = gettext.translation(
"messages", path / "l10n", languages=["en"])
def translate(message_name: str) -> str:
if not message_name:
return message_name
return translator.gettext(message_name)
except FileNotFoundError:
# TheChannel has no locale data for English.
def translate(message_name: str) -> str:
return message_name
return translator.gettext(message_name)
bind_gettext(translate)
src = beacons_lua.read_text()
@ -148,7 +157,6 @@ class Importer:
], indent=True))
def parse_args() -> argparse.Namespace:
"""Parses and returns command line arguments."""
parser = argparse.ArgumentParser()
@ -175,6 +183,7 @@ def parse_args() -> argparse.Namespace:
def main() -> None:
"""Program entry point."""
logging.basicConfig(level=logging.DEBUG)
args = parse_args()
Importer(args.dcs_path, args.export_to).run()