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]** Support for newly added BTR-82A, T-72B3
* **[Units]** Added ZSU-57 AAA sites * **[Units]** Added ZSU-57 AAA sites
* **[Culling]** BARCAP missions no longer create culling exclusion zones. * **[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: ## Fixes:
* **[Mission Generator]** Fix mission generation error when there are too many radio frequency to setup for the Mig-21 * **[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]** Fix ground units not moving forward
* **[Mission Generator]** Fixed assigned radio channels overlapping with beacons.
* **[Flight Planner]** Fix creation of custom waypoints. * **[Flight Planner]** Fix creation of custom waypoints.
* **[Campaigns]** Fixed many cases of SAMs spawning on the runways/taxiways in Syria Full.
# 2.3.1 # 2.3.1

View File

@ -199,10 +199,14 @@ class Operation:
@classmethod @classmethod
def create_radio_registries(cls) -> None: 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_tacan_registry(unique_map_frequencies)
cls._create_radio_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 @classmethod
def assign_channels_to_flights(cls, flights: List[FlightData], def assign_channels_to_flights(cls, flights: List[FlightData],
air_support: AirSupport) -> None: 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_fm)
unique_map_frequencies.add(data.atc.vhf_am) unique_map_frequencies.add(data.atc.vhf_am)
unique_map_frequencies.add(data.atc.uhf) unique_map_frequencies.add(data.atc.uhf)
# No need to reserve ILS or TACAN because those are in the # No need to reserve ILS or TACAN because those are in the
# beacon list. # beacon list.
@classmethod @classmethod
def _generate_ground_units(cls): def _generate_ground_units(cls):

View File

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

View File

@ -20,20 +20,20 @@ from dcs.planes import (
B_17G, B_17G,
B_52H, B_52H,
Bf_109K_4, Bf_109K_4,
C_101EB,
C_101CC, C_101CC,
C_101EB,
FW_190A8, FW_190A8,
FW_190D9, FW_190D9,
F_14B, F_14B,
I_16, I_16,
JF_17, JF_17,
Ju_88A4, Ju_88A4,
PlaneType,
P_47D_30, P_47D_30,
P_47D_30bl1, P_47D_30bl1,
P_47D_40, P_47D_40,
P_51D, P_51D,
P_51D_30_NA, P_51D_30_NA,
PlaneType,
SpitfireLFMkIX, SpitfireLFMkIX,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
Su_33, Su_33,
@ -59,16 +59,15 @@ from dcs.task import (
OptReactOnThreat, OptReactOnThreat,
OptRestrictJettison, OptRestrictJettison,
OrbitAction, OrbitAction,
PinpointStrike,
RunwayAttack, RunwayAttack,
SEAD, SEAD,
StartCommand, StartCommand,
Targets, Targets,
Task, Task,
WeaponType, WeaponType,
PinpointStrike,
) )
from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerRule from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unittype import FlyingType, UnitType 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.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData from gen.runways import RunwayData
from .conflictgen import Conflict
from .flights.flightplan import ( from .flights.flightplan import (
CasFlightPlan, CasFlightPlan,
LoiterFlightPlan, LoiterFlightPlan,
@ -108,7 +106,6 @@ from .flights.flightplan import (
) )
from .flights.traveltime import GroundSpeed, TotEstimator from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen from .naming import namegen
from .runways import RunwayAssigner
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -1349,7 +1346,7 @@ class AircraftConflictGenerator:
# And setting *our* waypoint TOT causes the takeoff time to show up in # And setting *our* waypoint TOT causes the takeoff time to show up in
# the player's kneeboard. # 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 # And finally assign it to the FlightData info so it shows correctly in
# the briefing. # the briefing.
self.flights[-1].departure_delay = start_time 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.""" """Iterates over all waypoints in the flight plan, in order."""
raise NotImplementedError raise NotImplementedError
@property def edges(
def edges(self) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]: self, until: Optional[FlightWaypoint] = None
) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
"""A list of all paths between waypoints, in order.""" """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, def best_speed_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> int: b: FlightWaypoint) -> int:
@ -137,7 +144,6 @@ class FlightPlan:
"""Joker fuel value for the FlightPlan """Joker fuel value for the FlightPlan
""" """
return self.bingo_fuel + 1000 return self.bingo_fuel + 1000
def max_distance_from(self, cp: ControlPoint) -> int: def max_distance_from(self, cp: ControlPoint) -> int:
"""Returns the farthest waypoint of the flight plan from a ControlPoint. """Returns the farthest waypoint of the flight plan from a ControlPoint.
@ -156,26 +162,18 @@ class FlightPlan:
""" """
return timedelta() 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( def _travel_time_to_waypoint(
self, destination: FlightWaypoint) -> timedelta: self, destination: FlightWaypoint) -> timedelta:
total = timedelta() total = timedelta()
for previous_waypoint, waypoint in self.edges:
total += self.travel_time_between_waypoints(previous_waypoint, if destination not in self.waypoints:
waypoint)
if waypoint == destination:
break
else:
raise PlanningError( raise PlanningError(
f"Did not find destination waypoint {destination} in " f"Did not find destination waypoint {destination} in "
f"waypoints for {self.flight}") 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 return total
def travel_time_between_waypoints(self, a: FlightWaypoint, def travel_time_between_waypoints(self, a: FlightWaypoint,
@ -196,10 +194,59 @@ class FlightPlan:
def dismiss_escort_at(self) -> Optional[FlightWaypoint]: def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
return None 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) @dataclass(frozen=True)
class LoiterFlightPlan(FlightPlan): class LoiterFlightPlan(FlightPlan):
hold: FlightWaypoint hold: FlightWaypoint
hold_duration: timedelta
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
raise NotImplementedError raise NotImplementedError
@ -221,6 +268,17 @@ class LoiterFlightPlan(FlightPlan):
return self.push_time return self.push_time
return None 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) @dataclass(frozen=True)
class FormationFlightPlan(LoiterFlightPlan): class FormationFlightPlan(LoiterFlightPlan):
@ -254,7 +312,7 @@ class FormationFlightPlan(LoiterFlightPlan):
all of its formation waypoints. all of its formation waypoints.
""" """
speeds = [] speeds = []
for previous_waypoint, waypoint in self.edges: for previous_waypoint, waypoint in self.edges():
if waypoint in self.package_speed_waypoints: if waypoint in self.package_speed_waypoints:
speeds.append(self.best_speed_between_waypoints( speeds.append(self.best_speed_between_waypoints(
previous_waypoint, waypoint)) previous_waypoint, waypoint))
@ -486,7 +544,7 @@ class StrikeFlightPlan(FormationFlightPlan):
"""The estimated time between the first waypoint and the target.""" """The estimated time between the first waypoint and the target."""
destination = self.tot_waypoint destination = self.tot_waypoint
total = timedelta() total = timedelta()
for previous_waypoint, waypoint in self.edges: for previous_waypoint, waypoint in self.edges():
if waypoint == self.tot_waypoint: if waypoint == self.tot_waypoint:
# For anything strike-like the TOT waypoint is the *flight's* # For anything strike-like the TOT waypoint is the *flight's*
# mission target, but to synchronize with the rest of the # mission target, but to synchronize with the rest of the
@ -846,6 +904,7 @@ class FlightPlanBuilder:
lead_time=timedelta(minutes=5), lead_time=timedelta(minutes=5),
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)), hold=builder.hold(self._hold_point(flight)),
hold_duration=timedelta(minutes=5),
sweep_start=start, sweep_start=start,
sweep_end=end, sweep_end=end,
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
@ -1050,6 +1109,7 @@ class FlightPlanBuilder:
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)), hold=builder.hold(self._hold_point(flight)),
hold_duration=timedelta(minutes=5),
join=builder.join(self.package.waypoints.join), join=builder.join(self.package.waypoints.join),
ingress=ingress, ingress=ingress,
targets=[target], targets=[target],
@ -1196,6 +1256,7 @@ class FlightPlanBuilder:
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)), hold=builder.hold(self._hold_point(flight)),
hold_duration=timedelta(minutes=5),
join=builder.join(self.package.waypoints.join), join=builder.join(self.package.waypoints.join),
ingress=builder.ingress(ingress_type, ingress=builder.ingress(ingress_type,
self.package.waypoints.ingress, location), self.package.waypoints.ingress, location),

View File

@ -89,68 +89,23 @@ class TravelTime:
# TODO: Most if not all of this should move into FlightPlan. # TODO: Most if not all of this should move into FlightPlan.
class TotEstimator: 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: def __init__(self, package: Package) -> None:
self.package = package self.package = package
def mission_start_time(self, flight: Flight) -> timedelta: @staticmethod
takeoff_time = self.takeoff_time_for_flight(flight) def mission_start_time(flight: Flight) -> timedelta:
if takeoff_time is None: startup_time = flight.flight_plan.startup_time()
if startup_time is None:
# Could not determine takeoff time, probably due to a custom flight # Could not determine takeoff time, probably due to a custom flight
# plan. Start immediately. # plan. Start immediately.
return timedelta() return timedelta()
return startup_time
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
def earliest_tot(self) -> timedelta: def earliest_tot(self) -> timedelta:
earliest_tot = max(( earliest_tot = max((
self.earliest_tot_for_flight(f) for f in self.package.flights 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, # Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we # 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. # Round up so we don't get negative start times.
return timedelta(seconds=math.ceil(earliest_tot.total_seconds())) 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. """Estimate fastest time from mission start to the target position.
For BARCAP flights, this is time to race track start. This ensures that 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 The earliest possible TOT for the given flight in seconds. Returns 0
if an ingress point cannot be found. if an ingress point cannot be found.
""" """
time_to_target = self.travel_time_to_target(flight) # Clear the TOT, calculate the startup time. Negating the result gives
if time_to_target is None: # 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}") logging.warning(f"Cannot estimate TOT for {flight}")
# Return 0 so this flight's travel time does not affect the rest # Return 0 so this flight's travel time does not affect the rest
# of the package. # of the package.
return timedelta() return timedelta()
# Account for TOT offsets for the flight plan. An offset of -2 minutes return -time
# 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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -52,14 +52,14 @@
"name": "", "name": "",
"callsign": "IBND", "callsign": "IBND",
"beacon_type": 14, "beacon_type": 14,
"hertz": 333800000, "hertz": 109900000,
"channel": null "channel": null
}, },
{ {
"name": "", "name": "",
"callsign": "IBND", "callsign": "IBND",
"beacon_type": 15, "beacon_type": 15,
"hertz": 333800000, "hertz": 109900000,
"channel": null "channel": null
}, },
{ {
@ -77,12 +77,19 @@
"channel": null "channel": null
}, },
{ {
"name": "BandarEJask", "name": "JASK",
"callsign": "JSK", "callsign": "JSK",
"beacon_type": 9, "beacon_type": 9,
"hertz": 349000000, "hertz": 349000,
"channel": null "channel": null
}, },
{
"name": "",
"callsign": "JSK",
"beacon_type": 5,
"hertz": null,
"channel": 110
},
{ {
"name": "BandarLengeh", "name": "BandarLengeh",
"callsign": "LEN", "callsign": "LEN",
@ -101,8 +108,8 @@
"name": "", "name": "",
"callsign": "MMA", "callsign": "MMA",
"beacon_type": 15, "beacon_type": 15,
"hertz": 111100000, "hertz": 109100000,
"channel": 48 "channel": 28
}, },
{ {
"name": "", "name": "",
@ -115,28 +122,28 @@
"name": "", "name": "",
"callsign": "IMA", "callsign": "IMA",
"beacon_type": 15, "beacon_type": 15,
"hertz": 109100000, "hertz": 111100000,
"channel": 28 "channel": 48
}, },
{ {
"name": "", "name": "",
"callsign": "RMA", "callsign": "RMA",
"beacon_type": 15, "beacon_type": 15,
"hertz": 114900000, "hertz": 108700000,
"channel": 24 "channel": 24
}, },
{ {
"name": "", "name": "",
"callsign": "MMA", "callsign": "MMA",
"beacon_type": 14, "beacon_type": 14,
"hertz": 111100000, "hertz": 109100000,
"channel": 48 "channel": 28
}, },
{ {
"name": "", "name": "",
"callsign": "RMA", "callsign": "RMA",
"beacon_type": 14, "beacon_type": 14,
"hertz": 114900000, "hertz": 108700000,
"channel": 24 "channel": 24
}, },
{ {
@ -150,8 +157,8 @@
"name": "", "name": "",
"callsign": "IMA", "callsign": "IMA",
"beacon_type": 14, "beacon_type": 14,
"hertz": 109100000, "hertz": 111100000,
"channel": 28 "channel": 48
}, },
{ {
"name": "AlDhafra", "name": "AlDhafra",
@ -332,7 +339,7 @@
"name": "KishIsland", "name": "KishIsland",
"callsign": "KIH", "callsign": "KIH",
"beacon_type": 9, "beacon_type": 9,
"hertz": 201000000, "hertz": 201000,
"channel": null "channel": null
}, },
{ {
@ -367,14 +374,14 @@
"name": "LavanIsland", "name": "LavanIsland",
"callsign": "LVA", "callsign": "LVA",
"beacon_type": 9, "beacon_type": 9,
"hertz": 310000000, "hertz": 310000,
"channel": 0 "channel": 0
}, },
{ {
"name": "LiwaAirbase", "name": "LiwaAirbase",
"callsign": "\u00c4\u00bc", "callsign": "OMLW",
"beacon_type": 7, "beacon_type": 6,
"hertz": null, "hertz": 117400000,
"channel": 121 "channel": 121
}, },
{ {
@ -433,13 +440,6 @@
"hertz": 113600000, "hertz": 113600000,
"channel": 83 "channel": 83
}, },
{
"name": "SasAlNakheelAirport",
"callsign": "SAS",
"beacon_type": 10,
"hertz": 128925,
"channel": null
},
{ {
"name": "SasAlNakheel", "name": "SasAlNakheel",
"callsign": "SAS", "callsign": "SAS",
@ -500,14 +500,14 @@
"name": "", "name": "",
"callsign": "ISYZ", "callsign": "ISYZ",
"beacon_type": 15, "beacon_type": 15,
"hertz": 109900000, "hertz": 108500000,
"channel": null "channel": null
}, },
{ {
"name": "", "name": "",
"callsign": "ISYZ", "callsign": "ISYZ",
"beacon_type": 14, "beacon_type": 14,
"hertz": 109900000, "hertz": 108500000,
"channel": null "channel": null
}, },
{ {
@ -556,7 +556,7 @@
"name": "DezfulAirport", "name": "DezfulAirport",
"callsign": "DZF", "callsign": "DZF",
"beacon_type": 9, "beacon_type": 9,
"hertz": 293000000, "hertz": 293000,
"channel": null "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 from contextlib import contextmanager
import dataclasses import dataclasses
import gettext import gettext
import logging
import os import os
from pathlib import Path from pathlib import Path
import textwrap 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]: def beacons_from_terrain(dcs_path: Path, path: Path) -> Iterable[Beacon]:
logging.info(f"Loading terrain data from {path}")
# TODO: Fix case-sensitive issues. # TODO: Fix case-sensitive issues.
# The beacons.lua file differs by case in some terrains. Will need to be # 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 # 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 end
""")) """))
translator = gettext.translation(
"messages", path / "l10n", languages=["en"])
def translate(message_name: str) -> str: try:
if not message_name: 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 message_name
return translator.gettext(message_name)
bind_gettext(translate) bind_gettext(translate)
src = beacons_lua.read_text() src = beacons_lua.read_text()
@ -148,7 +157,6 @@ class Importer:
], indent=True)) ], indent=True))
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
"""Parses and returns command line arguments.""" """Parses and returns command line arguments."""
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@ -175,6 +183,7 @@ def parse_args() -> argparse.Namespace:
def main() -> None: def main() -> None:
"""Program entry point.""" """Program entry point."""
logging.basicConfig(level=logging.DEBUG)
args = parse_args() args = parse_args()
Importer(args.dcs_path, args.export_to).run() Importer(args.dcs_path, args.export_to).run()