diff --git a/changelog.md b/changelog.md index 6d1e647d..a576b068 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/game/operation/operation.py b/game/operation/operation.py index d2c46f58..ec2a086a 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -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): diff --git a/game/version.py b/game/version.py index c9bfe293..0a4d2acb 100644 --- a/game/version.py +++ b/game/version.py @@ -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: diff --git a/gen/aircraft.py b/gen/aircraft.py index e51b0f2e..87233d35 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -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 diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 8d4e532f..478275cc 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -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), diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py index 714fbd25..078dabc7 100644 --- a/gen/flights/traveltime.py +++ b/gen/flights/traveltime.py @@ -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 diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py index 9f5df938..4aa3683c 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py @@ -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() diff --git a/resources/campaigns/syria_full_map_remastered.miz b/resources/campaigns/syria_full_map_remastered.miz index 4a490f5d..eecdc253 100644 Binary files a/resources/campaigns/syria_full_map_remastered.miz and b/resources/campaigns/syria_full_map_remastered.miz differ diff --git a/resources/dcs/beacons/caucasus.json b/resources/dcs/beacons/caucasus.json index d84fefc0..06eae1d2 100644 --- a/resources/dcs/beacons/caucasus.json +++ b/resources/dcs/beacons/caucasus.json @@ -1,1154 +1,1147 @@ [ { - "name": "", + "name": "Anapa-Vityazevo", "callsign": "AP", "beacon_type": 12, "hertz": 443000, "channel": null }, { - "name": "", + "name": "Anapa-Vityazevo", "callsign": "P", "beacon_type": 13, "hertz": 215000, "channel": null }, { - "name": "", + "name": "Anapa-Vityazevo", "callsign": "AN", "beacon_type": 12, "hertz": 443000, "channel": null }, { - "name": "", + "name": "Anapa-Vityazevo", "callsign": "N", "beacon_type": 13, "hertz": 215000, "channel": null }, { - "name": "", + "name": "Batumi", "callsign": "ILU", "beacon_type": 14, "hertz": 110300000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Batumi", + "callsign": "ILU", "beacon_type": 15, "hertz": 110300000, "channel": null }, { - "name": "", + "name": "Batumi", "callsign": "BTM", "beacon_type": 5, "hertz": 977000000, "channel": 16 }, { - "name": "", + "name": "Batumi", "callsign": "LU", "beacon_type": 10, "hertz": 430000, "channel": null }, { - "name": "", + "name": "Beslan", "callsign": "CX", "beacon_type": 12, "hertz": 1050000, "channel": null }, { - "name": "", + "name": "Beslan", "callsign": "C", "beacon_type": 13, "hertz": 250000, "channel": null }, { - "name": "", + "name": "Beslan", "callsign": "ICH", "beacon_type": 14, "hertz": 110500000, "channel": null }, { - "name": "", + "name": "Beslan", "callsign": "", "beacon_type": 15, "hertz": 110500000, "channel": null }, { - "name": "", + "name": "Gelendzhik", "callsign": "GN", "beacon_type": 10, "hertz": 1000000, "channel": null }, { - "name": "", + "name": "Gelendzhik", "callsign": "GN", "beacon_type": 2, "hertz": 114300000, "channel": 90 }, { - "name": "", + "name": "Gudauta", "callsign": "XC", "beacon_type": 11, "hertz": 395000, "channel": null }, { - "name": "", + "name": "Kobuleti", "callsign": "KT", "beacon_type": 12, "hertz": 870000, "channel": null }, { - "name": "", + "name": "Kobuleti", "callsign": "T", "beacon_type": 13, "hertz": 490000, "channel": null }, { - "name": "", + "name": "Kobuleti", "callsign": "IKB", "beacon_type": 14, "hertz": 111500000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Kobuleti", + "callsign": "IKB", "beacon_type": 15, "hertz": 111500000, "channel": null }, { - "name": "", + "name": "Kobuleti", "callsign": "KBL", "beacon_type": 5, "hertz": 1154000000, "channel": 67 }, { - "name": "", + "name": "Krasnodar-Center", "callsign": "OC", "beacon_type": 12, "hertz": 625000, "channel": null }, { - "name": "", + "name": "Krasnodar-Center", "callsign": "O", "beacon_type": 13, "hertz": 303000, "channel": null }, { - "name": "", + "name": "Krasnodar-Center", "callsign": "MB", "beacon_type": 12, "hertz": 625000, "channel": null }, { - "name": "", + "name": "Krasnodar-Center", "callsign": "M", "beacon_type": 13, "hertz": 303000, "channel": null }, { - "name": "", + "name": "Krasnodar-Center", "callsign": "MB", "beacon_type": 16, "hertz": 838000000, "channel": 38 }, { - "name": "", + "name": "Krasnodar-Center", "callsign": "MB", "beacon_type": 17, "hertz": 838000000, "channel": 38 }, { - "name": "", + "name": "Krasnodar-Center", "callsign": "MB", "beacon_type": 7, "hertz": 840000000, "channel": 40 }, { - "name": "", + "name": "Krasnodar-Pashkovsky", "callsign": "KR", "beacon_type": 12, "hertz": 493000, "channel": null }, { - "name": "", + "name": "Krasnodar-Pashkovsky", "callsign": "K", "beacon_type": 13, "hertz": 240000, "channel": null }, { - "name": "", + "name": "Krasnodar-Pashkovsky", "callsign": "LD", "beacon_type": 12, "hertz": 493000, "channel": null }, { - "name": "", + "name": "Krasnodar-Pashkovsky", "callsign": "L", "beacon_type": 13, "hertz": 240000, "channel": null }, { - "name": "", + "name": "Krasnodar-Pashkovsky", "callsign": "KN", "beacon_type": 2, "hertz": 115800000, "channel": 105 }, { - "name": "", + "name": "Krymsk", "callsign": "KW", "beacon_type": 12, "hertz": 408000, "channel": null }, { - "name": "", + "name": "Krymsk", "callsign": "K", "beacon_type": 13, "hertz": 803000, "channel": null }, { - "name": "", + "name": "Krymsk", "callsign": "OX", "beacon_type": 12, "hertz": 408000, "channel": null }, { - "name": "", + "name": "Krymsk", "callsign": "O", "beacon_type": 13, "hertz": 803000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Krymsk", + "callsign": "OX", "beacon_type": 17, "hertz": 826000000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Krymsk", + "callsign": "KW", "beacon_type": 17, "hertz": 826000000, "channel": null }, { - "name": "", + "name": "Krymsk", "callsign": "KW", "beacon_type": 16, "hertz": 826000000, "channel": 26 }, { - "name": "", + "name": "Krymsk", "callsign": "OX", "beacon_type": 16, "hertz": 826000000, "channel": 26 }, { - "name": "", + "name": "Krymsk", "callsign": "KW", "beacon_type": 7, "hertz": 828000000, "channel": 28 }, { - "name": "", + "name": "Kutaisi", "callsign": "IKS", "beacon_type": 14, "hertz": 109750000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Kutaisi", + "callsign": "IKS", "beacon_type": 15, "hertz": 109750000, "channel": null }, { - "name": "", + "name": "Kutaisi", "callsign": "TI", "beacon_type": 11, "hertz": 477000, "channel": null }, { - "name": "", + "name": "Kutaisi", "callsign": "KTS", "beacon_type": 5, "hertz": 1005000000, "channel": 44 }, { - "name": "KUTAISI", + "name": "Kutaisi", "callsign": "KT", "beacon_type": 2, "hertz": 113600000, "channel": 83 }, { - "name": "", + "name": "Tbilisi-Lochini", "callsign": "BP", "beacon_type": 12, "hertz": 342000, "channel": null }, { - "name": "", + "name": "Tbilisi-Lochini", "callsign": "B", "beacon_type": 13, "hertz": 923000, "channel": null }, { - "name": "", - "callsign": "INA", + "name": "Tbilisi-Lochini", + "callsign": "IVP", "beacon_type": 14, "hertz": 110300000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Tbilisi-Lochini", + "callsign": "IVP", "beacon_type": 15, "hertz": 110300000, "channel": null }, { - "name": "", + "name": "Tbilisi-Lochini", "callsign": "NA", "beacon_type": 12, "hertz": 211000, "channel": null }, { - "name": "", + "name": "Tbilisi-Lochini", "callsign": "N", "beacon_type": 13, "hertz": 435000, "channel": null }, { - "name": "", + "name": "Tbilisi-Lochini", "callsign": "INA", "beacon_type": 14, "hertz": 108900000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Tbilisi-Lochini", + "callsign": "INA", "beacon_type": 15, "hertz": 108900000, "channel": null }, { - "name": "", + "name": "Tbilisi-Lochini", "callsign": "TB", "beacon_type": 2, "hertz": 113700000, "channel": 84 }, { - "name": "", + "name": "Tbilisi-Lochini", "callsign": "GTB", "beacon_type": 5, "hertz": 986000000, "channel": 25 }, { - "name": "", + "name": "Maykop-Khanskaya", "callsign": "RK", "beacon_type": 12, "hertz": 289000, "channel": null }, { - "name": "", + "name": "Maykop-Khanskaya", "callsign": "R", "beacon_type": 13, "hertz": 591000, "channel": null }, { - "name": "", + "name": "Maykop-Khanskaya", "callsign": "DG", "beacon_type": 12, "hertz": 289000, "channel": null }, { - "name": "", + "name": "Maykop-Khanskaya", "callsign": "D", "beacon_type": 13, "hertz": 591000, "channel": null }, { - "name": "", + "name": "Maykop-Khanskaya", "callsign": "DG", "beacon_type": 16, "hertz": 836000000, "channel": 36 }, { - "name": "", + "name": "Maykop-Khanskaya", "callsign": "DG", "beacon_type": 17, "hertz": 836000000, "channel": 36 }, { - "name": "", + "name": "Maykop-Khanskaya", "callsign": "DG", "beacon_type": 7, "hertz": 834000000, "channel": 34 }, { - "name": "", + "name": "MineralnyeVody", "callsign": "NR", "beacon_type": 12, "hertz": 583000, "channel": null }, { - "name": "", + "name": "MineralnyeVody", "callsign": "N", "beacon_type": 13, "hertz": 283000, "channel": null }, { - "name": "", + "name": "MineralnyeVody", "callsign": "IMW", "beacon_type": 14, "hertz": 109300000, "channel": null }, { - "name": "", - "callsign": "", + "name": "MineralnyeVody", + "callsign": "IMW", "beacon_type": 15, "hertz": 109300000, "channel": null }, { - "name": "", + "name": "MineralnyeVody", "callsign": "MD", "beacon_type": 12, "hertz": 583000, "channel": null }, { - "name": "", + "name": "MineralnyeVody", "callsign": "D", "beacon_type": 13, "hertz": 283000, "channel": null }, { - "name": "", + "name": "MineralnyeVody", "callsign": "IMD", "beacon_type": 14, "hertz": 111700000, "channel": null }, { - "name": "", - "callsign": "", + "name": "MineralnyeVody", + "callsign": "IMD", "beacon_type": 15, "hertz": 111700000, "channel": null }, { - "name": "", + "name": "MineralnyeVody", "callsign": "MN", "beacon_type": 2, "hertz": 117100000, "channel": 118 }, { - "name": "", + "name": "Mozdok", "callsign": "DO", "beacon_type": 12, "hertz": 525000, "channel": null }, { - "name": "", + "name": "Mozdok", "callsign": "D", "beacon_type": 13, "hertz": 1065000, "channel": null }, { - "name": "", + "name": "Mozdok", "callsign": "RM", "beacon_type": 12, "hertz": 525000, "channel": null }, { - "name": "", + "name": "Mozdok", "callsign": "R", "beacon_type": 13, "hertz": 1065000, "channel": null }, { - "name": "", + "name": "Mozdok", "callsign": "MZ", "beacon_type": 16, "hertz": 822000000, "channel": 22 }, { - "name": "", - "callsign": "", + "name": "Mozdok", + "callsign": "MZ", "beacon_type": 17, "hertz": 822000000, "channel": 22 }, { - "name": "", - "callsign": "MZ", + "name": "Mozdok", + "callsign": "MK", "beacon_type": 16, "hertz": 822000000, "channel": 22 }, { - "name": "", - "callsign": "", + "name": "Mozdok", + "callsign": "MK", "beacon_type": 17, "hertz": 822000000, "channel": 22 }, { - "name": "", + "name": "Mozdok", "callsign": "MZ", "beacon_type": 7, "hertz": 820000000, "channel": 20 }, { - "name": "", + "name": "Nalchik", "callsign": "NL", "beacon_type": 12, "hertz": 718000, "channel": null }, { - "name": "", + "name": "Nalchik", "callsign": "N", "beacon_type": 13, "hertz": 350000, "channel": null }, { - "name": "", + "name": "Nalchik", "callsign": "INL", "beacon_type": 14, "hertz": 110500000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Nalchik", + "callsign": "INL", "beacon_type": 15, "hertz": 110500000, "channel": null }, { - "name": "", + "name": "Senaki-Kolkhi", "callsign": "BI", "beacon_type": 12, "hertz": 335000, "channel": null }, { - "name": "", + "name": "Senaki-Kolkhi", "callsign": "B", "beacon_type": 13, "hertz": 688000, "channel": null }, { - "name": "", + "name": "Senaki-Kolkhi", "callsign": "ITS", "beacon_type": 14, "hertz": 108900000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Senaki-Kolkhi", + "callsign": "ITS", "beacon_type": 15, "hertz": 108900000, "channel": null }, { - "name": "", + "name": "Senaki-Kolkhi", "callsign": "TSK", "beacon_type": 5, "hertz": 992000000, "channel": 31 }, { - "name": "", + "name": "Sochi-Adler", "callsign": "CO", "beacon_type": 11, "hertz": 761000, "channel": null }, { - "name": "", + "name": "Sochi-Adler", "callsign": "ISO", "beacon_type": 14, "hertz": 111100000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Sochi-Adler", + "callsign": "ISO", "beacon_type": 15, "hertz": 111100000, "channel": null }, { - "name": "", + "name": "Sukhumi-Babushara", "callsign": "AV", "beacon_type": 12, "hertz": 489000, "channel": null }, { - "name": "", + "name": "Sukhumi-Babushara", "callsign": "A", "beacon_type": 13, "hertz": 995000, "channel": null }, { - "name": "", + "name": "Vaziani", "callsign": "IVZ", "beacon_type": 14, "hertz": 108750000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Vaziani", + "callsign": "IVZ", "beacon_type": 15, "hertz": 108750000, "channel": null }, { - "name": "", - "callsign": "IVZ", + "name": "Vaziani", + "callsign": "IVI", "beacon_type": 14, "hertz": 108750000, "channel": null }, { - "name": "", - "callsign": "", + "name": "Vaziani", + "callsign": "IVI", "beacon_type": 15, "hertz": 108750000, "channel": null }, { - "name": "", + "name": "Vaziani", "callsign": "VAS", "beacon_type": 5, "hertz": 983000000, "channel": 22 }, { - "name": "", + "name": "Ust-Labinks", "callsign": "NZ", "beacon_type": 9, "hertz": 330000, "channel": null }, { - "name": "", + "name": "Chervonoglinskoye", "callsign": "AR", "beacon_type": 9, "hertz": 440000, "channel": null }, { - "name": "", + "name": "Dmitrovka", "callsign": "DM", "beacon_type": 9, "hertz": 690000, "channel": null }, { - "name": "", + "name": "Agoy", "callsign": "AG", "beacon_type": 9, "hertz": 381000, "channel": null }, { - "name": "", + "name": "Maykop", "callsign": "MA", "beacon_type": 9, "hertz": 682000, "channel": null }, { - "name": "", + "name": "Herson", "callsign": "HS", "beacon_type": 9, "hertz": 1065000, "channel": null }, { - "name": "", + "name": "Smolenkaya", "callsign": "SM", "beacon_type": 9, "hertz": 662000, "channel": null }, { - "name": "", + "name": "Kislovodsk", "callsign": "KW", "beacon_type": 9, "hertz": 995000, "channel": null }, { - "name": "", + "name": "Taganrog", "callsign": "TC", "beacon_type": 9, "hertz": 470000, "channel": null }, { - "name": "", + "name": "Feodosiya", "callsign": "IL", "beacon_type": 9, "hertz": 300500, "channel": null }, { - "name": "", + "name": "Shyriaeve", "callsign": "SH", "beacon_type": 9, "hertz": 389000, "channel": null }, { - "name": "", + "name": "Odessa", "callsign": "OD", "beacon_type": 9, "hertz": 348000, "channel": null }, { - "name": "", + "name": "Yalta", "callsign": "BS", "beacon_type": 9, "hertz": 300500, "channel": null }, { - "name": "", + "name": "Stavropol", "callsign": "KT", "beacon_type": 9, "hertz": 730000, "channel": null }, { - "name": "", + "name": "Yegorlykskaya", "callsign": "ER", "beacon_type": 9, "hertz": 435000, "channel": null }, { - "name": "", + "name": "Komisarivka", "callsign": "KM", "beacon_type": 9, "hertz": 950000, "channel": null }, { - "name": "", + "name": "Skadovsk", "callsign": "SK", "beacon_type": 9, "hertz": 680000, "channel": null }, { - "name": "", + "name": "Gali", "callsign": "DA", "beacon_type": 9, "hertz": 525000, "channel": null }, { - "name": "", + "name": "Mukhrani", "callsign": "DF", "beacon_type": 9, "hertz": 520000, "channel": null }, { - "name": "", + "name": "Ladozhskaya", "callsign": "RF", "beacon_type": 9, "hertz": 324000, "channel": null }, { - "name": "", - "callsign": "TP", + "name": "Teplorechensky", + "callsign": "FM", "beacon_type": 9, "hertz": 1182000, "channel": null }, { - "name": "", + "name": "Kalaus", "callsign": "BJ", "beacon_type": 9, "hertz": 735000, "channel": null }, { - "name": "", + "name": "Nikolaev-Kulbakino", "callsign": "NK", "beacon_type": 9, "hertz": 1030000, "channel": null }, { - "name": "", + "name": "Manychsky", "callsign": "MN", "beacon_type": 9, "hertz": 705000, "channel": null }, { - "name": "", + "name": "Kerch", "callsign": "KC", "beacon_type": 9, "hertz": 1050000, "channel": null }, { - "name": "", + "name": "TaganrogYuzhny", "callsign": "TY", "beacon_type": 9, "hertz": 720000, "channel": null }, { - "name": "", + "name": "Ali", "callsign": "AL", "beacon_type": 9, "hertz": 353000, "channel": null }, { - "name": "", - "callsign": "CA", + "name": "Elista", + "callsign": "SA", "beacon_type": 9, "hertz": 311000, "channel": null }, { - "name": "", + "name": "Ryazanskaya", "callsign": "XT", "beacon_type": 9, "hertz": 312000, "channel": null }, { - "name": "", + "name": "Kakhovka", "callsign": "KH", "beacon_type": 9, "hertz": 485000, "channel": null }, { - "name": "", + "name": "Vesely", "callsign": "WS", "beacon_type": 9, "hertz": 641000, "channel": null }, { - "name": "", + "name": "Odessa", "callsign": "WR", "beacon_type": 9, "hertz": 309500, "channel": null }, { - "name": "", + "name": "Armavir", "callsign": "VM", "beacon_type": 9, "hertz": 740000, "channel": null }, { - "name": "", + "name": "Grozny", "callsign": "WK", "beacon_type": 9, "hertz": 830000, "channel": null }, { - "name": "", + "name": "Tiraspol", "callsign": "TH", "beacon_type": 9, "hertz": 515000, "channel": null }, { - "name": "", + "name": "Simferopol", "callsign": "KC", "beacon_type": 9, "hertz": 580000, "channel": null }, { - "name": "", + "name": "Sultanskoye", "callsign": "SN", "beacon_type": 9, "hertz": 866000, "channel": null }, { - "name": "", + "name": "Buyalyk", "callsign": "DW", "beacon_type": 9, "hertz": 625000, "channel": null }, { - "name": "", + "name": "Sarmakovo", "callsign": "SR", "beacon_type": 9, "hertz": 907000, "channel": null }, { - "name": "", + "name": "Tendrovskiy", "callsign": "TD", "beacon_type": 9, "hertz": 309500, "channel": null }, { - "name": "", + "name": "Sukhoy", "callsign": "SH", "beacon_type": 9, "hertz": 862000, "channel": null }, { - "name": "", - "callsign": "SH", - "beacon_type": 9, - "hertz": 396000, - "channel": null - }, - { - "name": "", + "name": "Dzhubga", "callsign": "DV", "beacon_type": 9, "hertz": 420000, "channel": null }, { - "name": "", + "name": "Genichesk", "callsign": "GE", "beacon_type": 9, "hertz": 300500, "channel": null }, { - "name": "", + "name": "Primorsko-Akhtarsk", "callsign": "GW", "beacon_type": 9, "hertz": 920000, "channel": null }, { - "name": "", + "name": "Yasnaya", "callsign": "QG", "beacon_type": 9, "hertz": 435000, "channel": null }, { - "name": "", + "name": "Alushta", "callsign": "AL", "beacon_type": 9, "hertz": 384000, "channel": null }, { - "name": "", + "name": "Dobrushyn", "callsign": "DO", "beacon_type": 9, "hertz": 1175000, "channel": null }, { - "name": "", + "name": "Bolshevik", "callsign": "ND", "beacon_type": 9, "hertz": 507000, "channel": null }, { - "name": "", + "name": "Peredovay", "callsign": "PR", "beacon_type": 9, "hertz": 1210000, "channel": null }, { - "name": "", + "name": "Parutine", "callsign": "PA", "beacon_type": 9, "hertz": 905000, "channel": null }, { - "name": "", + "name": "Gori", "callsign": "OE", "beacon_type": 9, "hertz": 462000, "channel": null }, { - "name": "", + "name": "Liubymivka", "callsign": "LY", "beacon_type": 9, "hertz": 670000, "channel": null }, { - "name": "", + "name": "Mariupol", "callsign": "MA", "beacon_type": 9, "hertz": 770000, "channel": null }, { - "name": "", + "name": "Akhilleon", "callsign": "AH", "beacon_type": 9, "hertz": 300500, "channel": null }, { - "name": "", + "name": "Nikolaev-Matveyevka", "callsign": "NK", "beacon_type": 9, "hertz": 1030000, "channel": null }, { - "name": "", + "name": "Melitopol", "callsign": "NE", "beacon_type": 9, "hertz": 740000, "channel": null }, { - "name": "", + "name": "TchervonoLissya", "callsign": "LE", "beacon_type": 9, "hertz": 395000, "channel": null }, { - "name": "", + "name": "Tikhoretsk", "callsign": "UH", "beacon_type": 9, "hertz": 528000, "channel": null }, { - "name": "", + "name": "Rostov-Na-Donu", "callsign": "RE", "beacon_type": 9, "hertz": 320000, "channel": null }, { - "name": "", + "name": "Lazarevskoye", "callsign": "LA", "beacon_type": 9, "hertz": 307000, "channel": null }, { - "name": "", + "name": "Berdyansk", "callsign": "BD", "beacon_type": 9, "hertz": 342000, "channel": null }, { - "name": "", + "name": "Kropotkin", "callsign": "KP", "beacon_type": 9, "hertz": 214000, "channel": null }, { - "name": "", + "name": "Lymans-Ke", "callsign": "LA", "beacon_type": 9, "hertz": 750000, "channel": null }, { - "name": "", + "name": "Krasny", "callsign": "KS", "beacon_type": 9, "hertz": 1025000, diff --git a/resources/dcs/beacons/persiangulf.json b/resources/dcs/beacons/persiangulf.json index 3f0f870a..55e6ff19 100644 --- a/resources/dcs/beacons/persiangulf.json +++ b/resources/dcs/beacons/persiangulf.json @@ -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 }, { diff --git a/resources/factions/iraq_1991.json b/resources/factions/iraq_1991.json new file mode 100644 index 00000000..36dc0672 --- /dev/null +++ b/resources/factions/iraq_1991.json @@ -0,0 +1,85 @@ +{ + "country": "Iraq", + "name": "Iraq 1991", + "authors": "Hawkmoon", + "description": "
Iraq forces during desert Storm
", + "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" + ] +} diff --git a/resources/tools/import_beacons.py b/resources/tools/import_beacons.py index 9f3dd38e..8e1506ce 100644 --- a/resources/tools/import_beacons.py +++ b/resources/tools/import_beacons.py @@ -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()