diff --git a/changelog.md b/changelog.md
index 08acf714..c4d5b035 100644
--- a/changelog.md
+++ b/changelog.md
@@ -5,12 +5,20 @@
* **[Mission Generation]** Add option to switch ATFLIR to LITENING automatically for ground based F-18C flights
* **[Mission Generation]** Add option to configure OPFOR autoplanner aggressiveness and have the AI take risks and plan missions against defended targets
* **[Mission Generation]** Add option to configure the desired tanker on-station time in settings
+* **[Mission Generation]** Reserve GUARD frequency on VHF/UHF
+* **[Mission Generation]** Randomization in radio frequency allocation
* **[Cheat Menu]** Option to instantly transfer squadrons across bases.
* **[UI]** Add selectable units in faction overview during campaign generation.
* **[UI]** Add button to rename pilots in Air Wing's Squadron dialog.
* **[UI]** Add clone buttons for flights & packages.
* **[UI]** Editing of flight's custom name.
* **[UI]** Introduce custom names for packages (purely for organizational purposes).
+* **[UI]** Configurable UHF frequency (225-400MHz) for Packages, Carriers, LHAs, FOBs & FARPs.
+* **[UI]** Configurable Intra-Flight frequency for Flights.
+* **[UI]** Configurable TACAN for Carriers, LHAs & Tankers.
+* **[UI]** Configurable ICLS for capable Carriers & LHAs.
+* **[UI]** Configurable LINK4 for Carriers.
+* **[Kneeboard]** Show package information in Support page
## Fixes
* **[UI]** Removed deprecated options
diff --git a/game/ato/flight.py b/game/ato/flight.py
index e3c39463..4e19d7e3 100644
--- a/game/ato/flight.py
+++ b/game/ato/flight.py
@@ -12,6 +12,10 @@ from .flightroster import FlightRoster
from .flightstate import FlightState, Navigating, Uninitialized
from .flightstate.killed import Killed
from .loadouts import Loadout, Weapon
+from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
+from ..radio.TacanContainer import TacanContainer
+from ..radio.radios import RadioFrequency
+from ..radio.tacan import TacanChannel
from ..sidc import (
Entity,
SidcDescribable,
@@ -36,7 +40,7 @@ if TYPE_CHECKING:
F18_TGP_PYLON: int = 4
-class Flight(SidcDescribable):
+class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
def __init__(
self,
package: Package,
@@ -49,6 +53,9 @@ class Flight(SidcDescribable):
custom_name: Optional[str] = None,
cargo: Optional[TransferOrder] = None,
roster: Optional[FlightRoster] = None,
+ frequency: Optional[RadioFrequency] = None,
+ channel: Optional[TacanChannel] = None,
+ callsign: Optional[str] = None,
) -> None:
self.id = uuid.uuid4()
self.package = package
@@ -68,6 +75,11 @@ class Flight(SidcDescribable):
self.custom_name = custom_name
self.group_id: int = 0
+ self.frequency = frequency
+ if self.unit_type.dcs_unit_type.tacan:
+ self.tacan = channel
+ self.tcn_name = callsign
+
# Only used by transport missions.
self.cargo = cargo
diff --git a/game/ato/package.py b/game/ato/package.py
index b91c8a61..037231c6 100644
--- a/game/ato/package.py
+++ b/game/ato/package.py
@@ -13,13 +13,14 @@ from .flightplans.formation import FormationFlightPlan
from .flighttype import FlightType
from .packagewaypoints import PackageWaypoints
from .traveltime import TotEstimator
+from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
from ..radio.radios import RadioFrequency
if TYPE_CHECKING:
from game.theater import ControlPoint, MissionTarget
-class Package:
+class Package(RadioFrequencyContainer):
"""A mission package."""
def __init__(
diff --git a/game/migrator.py b/game/migrator.py
index 2242e89d..c5f40c79 100644
--- a/game/migrator.py
+++ b/game/migrator.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
from game.ato.packagewaypoints import PackageWaypoints
from game.data.doctrine import MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
@@ -9,6 +9,11 @@ if TYPE_CHECKING:
from game import Game
+def try_set_attr(obj: Any, attr_name: str, val: Any = None) -> None:
+ if not hasattr(obj, attr_name):
+ setattr(obj, attr_name, val)
+
+
class Migrator:
def __init__(self, game: Game):
self.game = game
@@ -18,6 +23,8 @@ class Migrator:
self._update_doctrine()
self._update_packagewaypoints()
self._update_package_attributes()
+ self._update_control_points()
+ self._update_flights()
def _update_doctrine(self) -> None:
doctrines = [
@@ -40,13 +47,32 @@ class Migrator:
def _update_packagewaypoints(self) -> None:
for c in self.game.coalitions:
for p in c.ato.packages:
- if p.flights and not hasattr(p.waypoints, "initial"):
- p.waypoints = PackageWaypoints.create(p, c)
+ if p.flights:
+ try_set_attr(p.waypoints, "initial", PackageWaypoints.create(p, c))
def _update_package_attributes(self) -> None:
for c in self.game.coalitions:
for p in c.ato.packages:
- if not hasattr(p, "custom_name"):
- p.custom_name = None
- if not hasattr(p, "frequency"):
- p.frequency = None
+ try_set_attr(p, "custom_name")
+ try_set_attr(p, "frequency")
+
+ def _update_control_points(self) -> None:
+ for cp in self.game.theater.controlpoints:
+ is_carrier = cp.is_carrier
+ is_lha = cp.is_lha
+ is_fob = cp.category == "fob"
+ radio_configurable = is_carrier or is_lha or is_fob
+ if radio_configurable:
+ try_set_attr(cp, "frequency")
+ if is_carrier or is_lha:
+ try_set_attr(cp, "tacan")
+ try_set_attr(cp, "tcn_name")
+ try_set_attr(cp, "icls_channel")
+ try_set_attr(cp, "icls_name")
+ try_set_attr(cp, "link4")
+
+ def _update_flights(self) -> None:
+ for f in self.game.db.flights.objects.values():
+ try_set_attr(f, "frequency")
+ try_set_attr(f, "tacan")
+ try_set_attr(f, "tcn_name")
diff --git a/game/missiongenerator/aircraft/aircraftgenerator.py b/game/missiongenerator/aircraft/aircraftgenerator.py
index 300af99b..cf4fdcbc 100644
--- a/game/missiongenerator/aircraft/aircraftgenerator.py
+++ b/game/missiongenerator/aircraft/aircraftgenerator.py
@@ -104,11 +104,7 @@ class AircraftGenerator:
ato: The ATO to spawn aircraft for.
dynamic_runways: Runway data for carriers and FARPs.
"""
- for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)):
- if package.frequency is None:
- continue
- if package.frequency not in self.radio_registry.allocated_channels:
- self.radio_registry.reserve(package.frequency)
+ self._reserve_frequencies_and_tacan(ato)
for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)):
if not package.flights:
@@ -206,3 +202,18 @@ class AircraftGenerator:
).configure()
)
return group
+
+ def _reserve_frequencies_and_tacan(self, ato: AirTaskingOrder) -> None:
+ for package in ato.packages:
+ if package.frequency is None:
+ continue
+ if package.frequency not in self.radio_registry.allocated_channels:
+ self.radio_registry.reserve(package.frequency)
+ for f in package.flights:
+ if (
+ f.frequency
+ and f.frequency not in self.radio_registry.allocated_channels
+ ):
+ self.radio_registry.reserve(f.frequency)
+ if f.tacan and f.tacan not in self.tacan_registy.allocated_channels:
+ self.tacan_registy.mark_unavailable(f.tacan)
diff --git a/game/missiongenerator/aircraft/flightgroupconfigurator.py b/game/missiongenerator/aircraft/flightgroupconfigurator.py
index 0654973a..e1764d8a 100644
--- a/game/missiongenerator/aircraft/flightgroupconfigurator.py
+++ b/game/missiongenerator/aircraft/flightgroupconfigurator.py
@@ -133,15 +133,16 @@ class FlightGroupConfigurator:
laser_codes.append(None)
def setup_radios(self) -> RadioFrequency:
- if (freq := self.flight.package.frequency) is None:
+ freq = self.flight.frequency
+ if freq is None and (freq := self.flight.package.frequency) is None:
freq = self.radio_registry.alloc_uhf()
self.flight.package.frequency = freq
- elif freq not in self.radio_registry.allocated_channels:
+ if freq not in self.radio_registry.allocated_channels:
self.radio_registry.reserve(freq)
if self.flight.flight_type in {FlightType.AEWC, FlightType.REFUELING}:
self.register_air_support(freq)
- elif self.flight.client_count:
+ elif self.flight.frequency is None and self.flight.client_count:
freq = self.flight.unit_type.alloc_flight_radio(self.radio_registry)
self.group.set_frequency(freq.mhz)
@@ -162,7 +163,12 @@ class FlightGroupConfigurator:
)
)
elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan):
- tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
+ if self.flight.tacan is None:
+ tacan = self.tacan_registry.alloc_for_band(
+ TacanBand.Y, TacanUsage.AirToAir
+ )
+ else:
+ tacan = self.flight.tacan
self.mission_data.tankers.append(
TankerInfo(
group_name=str(self.group.name),
diff --git a/game/missiongenerator/aircraft/waypoints/racetrack.py b/game/missiongenerator/aircraft/waypoints/racetrack.py
index e38c3734..a0722261 100644
--- a/game/missiongenerator/aircraft/waypoints/racetrack.py
+++ b/game/missiongenerator/aircraft/waypoints/racetrack.py
@@ -71,11 +71,14 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
if self.flight.unit_type.dcs_unit_type.tacan:
tanker_info = self.mission_data.tankers[-1]
tacan = tanker_info.tacan
- tacan_callsign = {
- "Texaco": "TEX",
- "Arco": "ARC",
- "Shell": "SHL",
- }.get(tanker_info.callsign)
+ if self.flight.tcn_name is None:
+ tacan_callsign = {
+ "Texaco": "TEX",
+ "Arco": "ARC",
+ "Shell": "SHL",
+ }.get(tanker_info.callsign)
+ else:
+ tacan_callsign = self.flight.tcn_name
waypoint.add_task(
ActivateBeaconCommand(
diff --git a/game/missiongenerator/missiongenerator.py b/game/missiongenerator/missiongenerator.py
index 622c02f3..0b9ad36a 100644
--- a/game/missiongenerator/missiongenerator.py
+++ b/game/missiongenerator/missiongenerator.py
@@ -37,6 +37,7 @@ from .missiondata import MissionData
from .tgogenerator import TgoGenerator
from .triggergenerator import TriggerGenerator
from .visualsgenerator import VisualsGenerator
+from ..radio.TacanContainer import TacanContainer
if TYPE_CHECKING:
from game import Game
@@ -177,6 +178,9 @@ class MissionGenerator:
logging.warning(f"TACAN beacon has no channel: {beacon.callsign}")
else:
self.tacan_registry.mark_unavailable(beacon.tacan_channel)
+ for cp in self.game.theater.controlpoints:
+ if isinstance(cp, TacanContainer) and cp.tacan is not None:
+ self.tacan_registry.mark_unavailable(cp.tacan)
def initialize_radio_registry(
self, unique_map_frequencies: set[RadioFrequency]
diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py
index 2352d12c..f47d5768 100644
--- a/game/missiongenerator/tgogenerator.py
+++ b/game/missiongenerator/tgogenerator.py
@@ -22,6 +22,8 @@ from dcs.ships import (
CVN_73,
CVN_75,
Stennis,
+ Forrestal,
+ LHA_Tarawa,
)
from dcs.statics import Fortification
from dcs.task import (
@@ -35,7 +37,7 @@ from dcs.task import (
)
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone
-from dcs.unit import Unit, InvisibleFARP
+from dcs.unit import Unit, InvisibleFARP, BaseFARP
from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import ShipType, VehicleType
from dcs.vehicles import vehicle_map
@@ -45,10 +47,16 @@ from game.missiongenerator.groundforcepainter import (
GroundForcePainter,
)
from game.missiongenerator.missiondata import CarrierInfo, MissionData
+from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
from game.runways import RunwayData
-from game.theater import ControlPoint, TheaterGroundObject, TheaterUnit
+from game.theater import (
+ ControlPoint,
+ TheaterGroundObject,
+ TheaterUnit,
+ NavalControlPoint,
+)
from game.theater.theatergroundobject import (
CarrierGroundObject,
GenericCarrierGroundObject,
@@ -351,7 +359,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
def __init__(
self,
ground_object: GenericCarrierGroundObject,
- control_point: ControlPoint,
+ control_point: NavalControlPoint,
country: Country,
game: Game,
mission: Mission,
@@ -372,9 +380,12 @@ class GenericCarrierGenerator(GroundObjectGenerator):
self.mission_data = mission_data
def generate(self) -> None:
-
- # This can also be refactored as the general generation was updated
- atc = self.radio_registry.alloc_uhf()
+ if self.control_point.frequency is not None:
+ atc = self.control_point.frequency
+ if atc not in self.radio_registry.allocated_channels:
+ self.radio_registry.reserve(atc)
+ else:
+ atc = self.radio_registry.alloc_uhf()
for g_id, group in enumerate(self.ground_object.groups):
if not group.units:
@@ -409,15 +420,33 @@ class GenericCarrierGenerator(GroundObjectGenerator):
f"Error generating carrier group for {self.control_point.name}"
)
ship_group.units[0].type = carrier_type.id
- tacan = self.tacan_registry.alloc_for_band(
- TacanBand.X, TacanUsage.TransmitReceive
- )
- tacan_callsign = self.tacan_callsign()
- icls = next(self.icls_alloc)
+ if self.control_point.tacan is None:
+ tacan = self.tacan_registry.alloc_for_band(
+ TacanBand.X, TacanUsage.TransmitReceive
+ )
+ else:
+ tacan = self.control_point.tacan
+ if self.control_point.tcn_name is None:
+ tacan_callsign = self.tacan_callsign()
+ else:
+ tacan_callsign = self.control_point.tcn_name
link4 = None
- if carrier_type in [Stennis, CVN_71, CVN_72, CVN_73, CVN_75]:
- link4 = self.radio_registry.alloc_uhf()
- self.activate_beacons(ship_group, tacan, tacan_callsign, icls, link4)
+ link4carriers = [Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal]
+ if carrier_type in link4carriers:
+ if self.control_point.link4 is None:
+ link4 = self.radio_registry.alloc_uhf()
+ else:
+ link4 = self.control_point.link4
+ icls = None
+ icls_name = self.control_point.icls_name
+ if carrier_type in link4carriers or carrier_type == LHA_Tarawa:
+ if self.control_point.icls_channel is None:
+ icls = next(self.icls_alloc)
+ else:
+ icls = self.control_point.icls_channel
+ self.activate_beacons(
+ ship_group, tacan, tacan_callsign, icls, icls_name, link4
+ )
self.add_runway_data(
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
)
@@ -461,7 +490,8 @@ class GenericCarrierGenerator(GroundObjectGenerator):
group: ShipGroup,
tacan: TacanChannel,
callsign: str,
- icls: int,
+ icls: Optional[int] = None,
+ icls_name: Optional[str] = None,
link4: Optional[RadioFrequency] = None,
) -> None:
group.points[0].tasks.append(
@@ -473,12 +503,14 @@ class GenericCarrierGenerator(GroundObjectGenerator):
aa=False,
)
)
- group.points[0].tasks.append(
- ActivateICLSCommand(icls, unit_id=group.units[0].id)
- )
+ if icls is not None:
+ icls_name = "" if icls_name is None else icls_name
+ group.points[0].tasks.append(
+ ActivateICLSCommand(icls, group.units[0].id, icls_name)
+ )
if link4 is not None:
group.points[0].tasks.append(
- ActivateLink4Command(int(link4.mhz), group.units[0].id)
+ ActivateLink4Command(link4.hertz, group.units[0].id)
)
group.points[0].tasks.append(ActivateACLSCommand(unit_id=group.units[0].id))
@@ -488,7 +520,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
atc: RadioFrequency,
tacan: TacanChannel,
callsign: str,
- icls: int,
+ icls: Optional[int],
) -> None:
# TODO: Make unit name usable.
# This relies on one control point mapping exactly
@@ -590,6 +622,13 @@ class HelipadGenerator:
self.helipads.add_unit(
InvisibleFARP(self.m.terrain, self.m.next_unit_id(), name_i)
)
+
+ # Set FREQ
+ if isinstance(self.cp, RadioFrequencyContainer) and self.cp.frequency:
+ for hp in self.helipads.units:
+ if isinstance(hp, BaseFARP):
+ hp.heliport_frequency = self.cp.frequency.mhz
+
pad = self.helipads.units[-1]
pad.position = helipad
pad.heading = heading
@@ -661,7 +700,9 @@ class TgoGenerator:
for ground_object in cp.ground_objects:
generator: GroundObjectGenerator
- if isinstance(ground_object, CarrierGroundObject):
+ if isinstance(ground_object, CarrierGroundObject) and isinstance(
+ cp, NavalControlPoint
+ ):
generator = CarrierGenerator(
ground_object,
cp,
@@ -675,7 +716,9 @@ class TgoGenerator:
self.unit_map,
self.mission_data,
)
- elif isinstance(ground_object, LhaGroundObject):
+ elif isinstance(ground_object, LhaGroundObject) and isinstance(
+ cp, NavalControlPoint
+ ):
generator = LhaGenerator(
ground_object,
cp,
diff --git a/game/radio/ICLSContainer.py b/game/radio/ICLSContainer.py
new file mode 100644
index 00000000..34c403ac
--- /dev/null
+++ b/game/radio/ICLSContainer.py
@@ -0,0 +1,6 @@
+from typing import Optional
+
+
+class ICLSContainer:
+ icls_channel: Optional[int] = None
+ icls_name: Optional[str] = None
diff --git a/game/radio/Link4Container.py b/game/radio/Link4Container.py
new file mode 100644
index 00000000..e872cc22
--- /dev/null
+++ b/game/radio/Link4Container.py
@@ -0,0 +1,8 @@
+from typing import Optional
+
+from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
+from game.radio.radios import RadioFrequency
+
+
+class Link4Container(RadioFrequencyContainer):
+ link4: Optional[RadioFrequency] = None
diff --git a/game/radio/RadioFrequencyContainer.py b/game/radio/RadioFrequencyContainer.py
new file mode 100644
index 00000000..9882cf70
--- /dev/null
+++ b/game/radio/RadioFrequencyContainer.py
@@ -0,0 +1,7 @@
+from typing import Optional
+
+from game.radio.radios import RadioFrequency
+
+
+class RadioFrequencyContainer:
+ frequency: Optional[RadioFrequency] = None
diff --git a/game/radio/TacanContainer.py b/game/radio/TacanContainer.py
new file mode 100644
index 00000000..a851cfe2
--- /dev/null
+++ b/game/radio/TacanContainer.py
@@ -0,0 +1,8 @@
+from typing import Optional
+
+from game.radio.tacan import TacanChannel
+
+
+class TacanContainer:
+ tacan: Optional[TacanChannel] = None
+ tcn_name: Optional[str] = None
diff --git a/game/radio/radios.py b/game/radio/radios.py
index 2d61a076..47860d50 100644
--- a/game/radio/radios.py
+++ b/game/radio/radios.py
@@ -3,6 +3,7 @@ from __future__ import annotations
import itertools
import logging
+import random
import re
from dataclasses import dataclass
from typing import Dict, FrozenSet, Iterator, List, Set, Tuple
@@ -156,57 +157,61 @@ 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", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
- Radio("AN/ARC-186(V) AM", (RadioRange(MHz(116), MHz(152), MHz(1), Modulation.AM),)),
- Radio("AN/ARC-186(V) FM", (RadioRange(MHz(30), MHz(76), MHz(1), Modulation.FM),)),
+ Radio("AN/ARC-164", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
+ Radio(
+ "AN/ARC-186(V) AM", (RadioRange(MHz(116), MHz(152), kHz(25), Modulation.AM),)
+ ),
+ Radio("AN/ARC-186(V) FM", (RadioRange(MHz(30), MHz(76), kHz(25), Modulation.FM),)),
Radio(
"AN/ARC-210",
(
RadioRange(
MHz(225),
MHz(400),
- MHz(1),
+ kHz(25),
Modulation.AM,
frozenset((MHz(243),)),
),
- RadioRange(MHz(136), MHz(155), MHz(1), Modulation.AM),
- RadioRange(MHz(156), MHz(174), MHz(1), Modulation.FM),
- RadioRange(MHz(118), MHz(136), MHz(1), Modulation.AM),
- RadioRange(MHz(30), MHz(88), MHz(1), Modulation.FM),
+ RadioRange(MHz(136), MHz(155), kHz(25), Modulation.AM),
+ RadioRange(MHz(156), MHz(174), kHz(25), Modulation.FM),
+ RadioRange(MHz(118), MHz(136), kHz(25), Modulation.AM),
+ RadioRange(MHz(30), MHz(88), kHz(25), Modulation.FM),
# The AN/ARC-210 can also use 225-400 and 136-155 with FM Modulation
RadioRange(
MHz(225),
MHz(400),
- MHz(1),
+ kHz(25),
Modulation.FM,
frozenset((MHz(243),)),
),
- RadioRange(MHz(136), MHz(155), MHz(1), Modulation.FM),
+ RadioRange(MHz(136), MHz(155), kHz(25), Modulation.FM),
),
),
- Radio("AN/ARC-222", (RadioRange(MHz(116), MHz(152), MHz(1), Modulation.AM),)),
- Radio("SCR-522", (RadioRange(MHz(100), MHz(156), MHz(1), Modulation.AM),)),
- Radio("A.R.I. 1063", (RadioRange(MHz(100), MHz(156), MHz(1), Modulation.AM),)),
+ Radio("AN/ARC-222", (RadioRange(MHz(116), MHz(152), kHz(25), Modulation.AM),)),
+ Radio("SCR-522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
+ Radio("A.R.I. 1063", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
Radio("BC-1206", (RadioRange(kHz(200), kHz(400), kHz(10), Modulation.AM),)),
Radio(
"TRT ERA 7000 V/UHF",
(
- RadioRange(MHz(118), MHz(150), MHz(1), Modulation.AM),
- RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),
+ RadioRange(MHz(118), MHz(150), kHz(25), Modulation.AM),
+ RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),
),
),
- Radio("TRT ERA 7200 UHF", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
+ Radio(
+ "TRT ERA 7200 UHF", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)
+ ),
# Tomcat radios
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
- Radio("AN/ARC-159", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
+ Radio("AN/ARC-159", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
Radio(
"AN/ARC-182",
(
- RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),
- RadioRange(MHz(108), MHz(174), MHz(1), Modulation.AM),
+ RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),
+ RadioRange(MHz(108), MHz(174), kHz(25), Modulation.AM),
# The Range from 30-88MHz should be FM but its modeled as AM in dcs
- RadioRange(MHz(30), MHz(88), MHz(1), Modulation.AM),
+ RadioRange(MHz(30), MHz(88), kHz(25), Modulation.AM),
),
),
Radio(
@@ -220,14 +225,14 @@ RADIOS: List[Radio] = [
# 4 preset channels (A/B/C/D)
Radio("SCR522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
# JF-17 Radios should use AM
- Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), MHz(1), Modulation.AM),)),
- Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
+ Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), kHz(25), Modulation.AM),)),
+ Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
# MiG-15bis
Radio("RSI-6K HF", (RadioRange(MHz(3, 750), MHz(5), kHz(25), Modulation.AM),)),
# MiG-19P
- Radio("RSIU-4V", (RadioRange(MHz(100), MHz(150), MHz(1), Modulation.AM),)),
+ Radio("RSIU-4V", (RadioRange(MHz(100), MHz(150), kHz(25), Modulation.AM),)),
# MiG-21bis
- Radio("RSIU-5V", (RadioRange(MHz(118), MHz(140), MHz(1), Modulation.AM),)),
+ Radio("RSIU-5V", (RadioRange(MHz(118), MHz(140), kHz(25), Modulation.AM),)),
# Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
Radio("R-800L1", (RadioRange(MHz(220), MHz(400), kHz(25), Modulation.AM),)),
@@ -263,7 +268,7 @@ RADIOS: List[Radio] = [
RadioRange(
MHz(225),
MHz(400),
- MHz(1),
+ kHz(25),
Modulation.AM,
frozenset((MHz(243),)),
),
@@ -275,28 +280,28 @@ RADIOS: List[Radio] = [
RadioRange(
MHz(30),
MHz(88),
- MHz(1),
+ kHz(25),
Modulation.FM,
frozenset((MHz(40, 500),)),
),
RadioRange(
MHz(108),
MHz(156),
- MHz(1),
+ kHz(25),
Modulation.AM,
frozenset((MHz(121, 500),)),
),
RadioRange(
MHz(156),
MHz(174),
- MHz(1),
+ kHz(25),
Modulation.FM,
frozenset((MHz(156, 800),)),
),
RadioRange(
MHz(225),
MHz(400),
- MHz(1),
+ kHz(25),
Modulation.AM, # Actually AM/FM, but we can't represent that.
frozenset((MHz(243),)),
),
@@ -323,6 +328,13 @@ def get_radio(name: str) -> Radio:
raise KeyError(f"Unknown radio: {name}")
+def random_frequency(radio: Radio) -> RadioFrequency:
+ range: RadioRange = radio.ranges[0]
+ delta = round(random.random() * (range.maximum.hertz - range.minimum.hertz))
+ delta -= delta % range.step.hertz
+ return RadioFrequency(range.minimum.hertz + delta)
+
+
class RadioRegistry:
"""Manages allocation of radio channels.
@@ -368,9 +380,8 @@ class RadioRegistry:
OutOfChannelsError: All channels compatible with the given radio are
already allocated.
"""
- allocator = self.radio_allocators[radio]
try:
- while (channel := next(allocator)) in self.allocated_channels:
+ while (channel := random_frequency(radio)) in self.allocated_channels:
pass
self.reserve(channel)
return channel
diff --git a/game/radio/tacan.py b/game/radio/tacan.py
index fb7bd42f..c9200391 100644
--- a/game/radio/tacan.py
+++ b/game/radio/tacan.py
@@ -54,8 +54,8 @@ class TacanChannel:
if match is None:
raise ValueError(f"Could not parse TACAN from {text}")
number = int(match.group(1))
- if not number:
- raise ValueError("TACAN channel cannot be 0")
+ if not (0 < number <= 126):
+ raise ValueError("TACAN channel cannot be 0 or larger than 126")
return TacanChannel(number, TacanBand(match.group(2)))
diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py
index a80dadef..976cbde5 100644
--- a/game/theater/controlpoint.py
+++ b/game/theater/controlpoint.py
@@ -22,6 +22,7 @@ from typing import (
TYPE_CHECKING,
Tuple,
Type,
+ Union,
)
from uuid import UUID
@@ -73,6 +74,10 @@ from ..data.units import UnitClass
from ..db import Database
from ..dcs.aircrafttype import AircraftType
from ..dcs.groundunittype import GroundUnitType
+from ..radio.ICLSContainer import ICLSContainer
+from ..radio.Link4Container import Link4Container
+from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
+from ..radio.TacanContainer import TacanContainer
from ..utils import nautical_miles
from ..weather import Conditions
@@ -305,7 +310,7 @@ class ControlPointStatus(IntEnum):
Destroyed = auto()
-StartingPosition = ShipGroup | StaticGroup | Airport | Point
+StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
class ControlPoint(MissionTarget, SidcDescribable, ABC):
@@ -1161,7 +1166,9 @@ class Airfield(ControlPoint):
return ControlPointStatus.Functional
-class NavalControlPoint(ControlPoint, ABC):
+class NavalControlPoint(
+ ControlPoint, ABC, Link4Container, TacanContainer, ICLSContainer
+):
@property
def is_fleet(self) -> bool:
return True
@@ -1392,7 +1399,7 @@ class OffMapSpawn(ControlPoint):
return ControlPointStatus.Functional
-class Fob(ControlPoint):
+class Fob(ControlPoint, RadioFrequencyContainer):
def __init__(
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
):
diff --git a/qt_ui/models.py b/qt_ui/models.py
index 51c2c87b..302060ff 100644
--- a/qt_ui/models.py
+++ b/qt_ui/models.py
@@ -17,9 +17,13 @@ from game.ato.flight import Flight
from game.ato.flighttype import FlightType
from game.ato.package import Package
from game.game import Game
+from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
+from game.radio.radios import RadioFrequency
+from game.radio.tacan import TacanChannel
from game.server import EventStream
from game.sim.gameupdateevents import GameUpdateEvents
from game.squadrons.squadron import Pilot, Squadron
+from game.theater import NavalControlPoint
from game.theater.missiontarget import MissionTarget
from game.transfers import PendingTransfers, TransferOrder
from qt_ui.simcontroller import SimController
@@ -179,6 +183,8 @@ class PackageModel(QAbstractListModel):
self.package.remove_flight(flight)
self.endRemoveRows()
self.update_tot()
+ self.game_model.release_freq(flight.frequency)
+ self.game_model.release_tacan(flight.tacan)
def flight_at_index(self, index: QModelIndex) -> Flight:
"""Returns the flight located at the given index."""
@@ -294,6 +300,7 @@ class AtoModel(QAbstractListModel):
# noinspection PyUnresolvedReferences
self.client_slots_changed.emit()
self.on_packages_changed()
+ self.release_comms_for_package(package)
def on_packages_changed(self) -> None:
if self.game is not None:
@@ -342,6 +349,12 @@ class AtoModel(QAbstractListModel):
def on_sim_update(self, _events: GameUpdateEvents) -> None:
self.dataChanged.emit(self.index(0), self.index(self.rowCount()))
+ def release_comms_for_package(self, package: Package):
+ self.game_model.release_freq(package.frequency)
+ for f in package.flights:
+ self.game_model.release_freq(f.frequency)
+ self.game_model.release_tacan(f.tacan)
+
class TransferModel(QAbstractListModel):
"""The model for a ground unit transfer."""
@@ -532,6 +545,12 @@ class GameModel:
self.ato_model = AtoModel(self, self.game.blue.ato)
self.red_ato_model = AtoModel(self, self.game.red.ato)
+ # For UI purposes
+ self.allocated_freqs: list[RadioFrequency] = list()
+ self.allocated_tacan: list[TacanChannel] = list()
+ self.allocated_icls: list[int] = list()
+ self.init_comms_registry()
+
def ato_model_for(self, player: bool) -> AtoModel:
if player:
return self.ato_model
@@ -553,3 +572,43 @@ class GameModel:
if self.game is None:
raise RuntimeError("GameModel has no Game set")
return self.game
+
+ def init_comms_registry(self) -> None:
+ if self.game is None:
+ return
+ allocated_freqs = {None}
+ allocated_tacan = {None}
+ allocated_icls = {None}
+ for p in self.ato_model.ato.packages:
+ allocated_freqs.add(p.frequency)
+ for f in p.flights:
+ allocated_freqs.add(f.frequency)
+ allocated_tacan.add(f.tacan)
+ for cp in self.game.theater.control_points_for(True):
+ if isinstance(cp, NavalControlPoint):
+ allocated_freqs.add(cp.frequency)
+ allocated_freqs.add(cp.link4)
+ allocated_tacan.add(cp.tacan)
+ allocated_icls.add(cp.icls_channel)
+ elif isinstance(cp, RadioFrequencyContainer):
+ allocated_freqs.add(cp.frequency)
+ allocated_freqs.remove(None)
+ allocated_tacan.remove(None)
+ allocated_icls.remove(None)
+ self.allocated_freqs = list(allocated_freqs)
+ self.allocated_tacan = list(allocated_tacan)
+ self.allocated_icls = list(allocated_icls)
+
+ def release_freq(self, freq: RadioFrequency):
+ if freq:
+ try:
+ self.allocated_freqs.remove(freq)
+ except ValueError:
+ pass
+
+ def release_tacan(self, tacan: TacanChannel):
+ if tacan:
+ try:
+ self.allocated_tacan.remove(tacan)
+ except ValueError:
+ pass
diff --git a/qt_ui/widgets/QFrequencyWidget.py b/qt_ui/widgets/QFrequencyWidget.py
new file mode 100644
index 00000000..30abddda
--- /dev/null
+++ b/qt_ui/widgets/QFrequencyWidget.py
@@ -0,0 +1,101 @@
+from PySide2.QtCore import Signal
+from PySide2.QtWidgets import (
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QWidget,
+)
+
+from game.ato import Flight
+from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
+from game.radio.radios import RadioFrequency, RadioRange, MHz, kHz
+from qt_ui.models import GameModel
+from qt_ui.windows.QRadioFrequencyDialog import QRadioFrequencyDialog
+
+
+class QFrequencyWidget(QWidget):
+
+ freq_changed = Signal(QWidget)
+
+ def __init__(
+ self, container: RadioFrequencyContainer, game_model: GameModel
+ ) -> None:
+ super().__init__()
+
+ self.ct = container
+ self.gm = game_model
+
+ columns = QHBoxLayout()
+ self.setLayout(columns)
+
+ self.freq = QLabel(self._get_label_text())
+ self.check_freq()
+ columns.addWidget(self.freq)
+ columns.addStretch()
+
+ self.set_freq_btn = QPushButton("Set FREQ")
+ self.set_freq_btn.setProperty("class", "comms")
+ self.set_freq_btn.setFixedWidth(100)
+ columns.addWidget(self.set_freq_btn)
+ self.set_freq_btn.clicked.connect(self.open_freq_dialog)
+
+ self.reset_freq_btn = QPushButton("Reset FREQ")
+ self.reset_freq_btn.setProperty("class", "btn-danger comms")
+ self.reset_freq_btn.setFixedWidth(100)
+ columns.addWidget(self.reset_freq_btn)
+ self.reset_freq_btn.clicked.connect(self.reset_freq)
+
+ def _get_label_text(self) -> str:
+ freq = "AUTO" if self.ct.frequency is None else self.ct.frequency
+ return f"FREQ: {freq}"
+
+ def open_freq_dialog(self) -> None:
+ range = RadioRange(MHz(30), MHz(400), kHz(25))
+ if isinstance(self.ct, Flight):
+ if self.ct.unit_type.intra_flight_radio is not None:
+ range = self.ct.unit_type.intra_flight_radio.ranges[0]
+ self.frequency_dialog = QRadioFrequencyDialog(self, self.ct, range)
+ self.frequency_dialog.accepted.connect(self.assign_frequency)
+ self.frequency_dialog.show()
+
+ def assign_frequency(self) -> None:
+ hz = round(self.frequency_dialog.frequency_input.value() * 10**6)
+ self._try_remove()
+ self.ct.frequency = RadioFrequency(hertz=hz)
+ self.gm.allocated_freqs.append(self.ct.frequency)
+ self.freq.setText(self._get_label_text())
+ self.check_freq()
+ self.freq_changed.emit(self)
+
+ def reset_freq(self) -> None:
+ self._try_remove()
+ self.ct.frequency = None
+ self.freq.setText(self._get_label_text())
+ self._reset_color_and_tooltip()
+ self.freq_changed.emit(self)
+
+ def check_freq(self) -> None:
+ if self.ct.frequency is None:
+ return
+ if self.gm.allocated_freqs.count(self.ct.frequency) > 1:
+ self.freq.setStyleSheet("color: orange")
+ self.freq.setToolTip(
+ "Double booked frequency, verify that this was your intention."
+ )
+ elif self.gm.allocated_freqs.count(self.ct.frequency) == 1:
+ self._reset_color_and_tooltip()
+ if self.ct.frequency == MHz(243) or self.ct.frequency == MHz(121, 500):
+ self.freq.setStyleSheet("color: red")
+ self.freq.setToolTip(
+ "GUARD Freq. was assigned, verify that this was your intention."
+ )
+
+ def _reset_color_and_tooltip(self):
+ self.freq.setStyleSheet("color: white")
+ self.freq.setToolTip(None)
+
+ def _try_remove(self) -> None:
+ try:
+ self.gm.allocated_freqs.remove(self.ct.frequency)
+ except ValueError:
+ pass
diff --git a/qt_ui/widgets/QICLSWidget.py b/qt_ui/widgets/QICLSWidget.py
new file mode 100644
index 00000000..ffcb08ea
--- /dev/null
+++ b/qt_ui/widgets/QICLSWidget.py
@@ -0,0 +1,86 @@
+from PySide2.QtWidgets import (
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QWidget,
+)
+
+from game.radio.ICLSContainer import ICLSContainer
+from qt_ui.models import GameModel
+from qt_ui.windows.QICLSDialog import QICLSDialog
+
+
+class QICLSWidget(QWidget):
+ def __init__(self, container: ICLSContainer, game_model: GameModel) -> None:
+ super().__init__()
+
+ self.ct = container
+ self.gm = game_model
+
+ columns = QHBoxLayout()
+ self.setLayout(columns)
+
+ self.channel = QLabel(self._get_label_text())
+ self.check_channel()
+ columns.addWidget(self.channel)
+ columns.addStretch()
+
+ self.set_icls_btn = QPushButton("Set ICLS")
+ self.set_icls_btn.setProperty("class", "comms")
+ self.set_icls_btn.setFixedWidth(100)
+ columns.addWidget(self.set_icls_btn)
+ self.set_icls_btn.clicked.connect(self.open_icls_dialog)
+
+ self.reset_icls_btn = QPushButton("Reset ICLS")
+ self.reset_icls_btn.setProperty("class", "btn-danger comms")
+ self.reset_icls_btn.setFixedWidth(100)
+ columns.addWidget(self.reset_icls_btn)
+ self.reset_icls_btn.clicked.connect(self.reset_icls)
+
+ def _get_label_text(self) -> str:
+ c = "AUTO" if self.ct.icls_channel is None else self.ct.icls_channel
+ cs = self.ct.icls_name
+ cs = "" if cs is None else f" ({cs})"
+ return f"ICLS: {c}{cs}"
+
+ def open_icls_dialog(self) -> None:
+ self.icls_dialog = QICLSDialog(self, self.ct)
+ self.icls_dialog.accepted.connect(self.assign_icls)
+ self.icls_dialog.show()
+
+ def assign_icls(self) -> None:
+ self._try_remove()
+ self.ct.icls_channel = self.icls_dialog.icls_input.value()
+ self.gm.allocated_icls.append(self.ct.icls_channel)
+ if cs := self.icls_dialog.callsign_input.text():
+ self.ct.icls_name = cs.upper()
+ self.channel.setText(self._get_label_text())
+ self.check_channel()
+
+ def reset_icls(self) -> None:
+ self._try_remove()
+ self.ct.icls_channel = None
+ self.ct.icls_name = None
+ self.channel.setText(self._get_label_text())
+ self._reset_color_and_tooltip()
+
+ def check_channel(self) -> None:
+ if self.ct.icls_channel is None:
+ return
+ if self.gm.allocated_icls.count(self.ct.icls_channel) > 1:
+ self.channel.setStyleSheet("color: orange")
+ self.channel.setToolTip(
+ "Double booked ICLS channel, verify that this was your intention."
+ )
+ elif self.gm.allocated_icls.count(self.ct.icls_channel) == 1:
+ self._reset_color_and_tooltip()
+
+ def _reset_color_and_tooltip(self):
+ self.channel.setStyleSheet("color: white")
+ self.channel.setToolTip(None)
+
+ def _try_remove(self) -> None:
+ try:
+ self.gm.allocated_icls.remove(self.ct.icls_channel)
+ except ValueError:
+ pass
diff --git a/qt_ui/widgets/QLink4Widget.py b/qt_ui/widgets/QLink4Widget.py
new file mode 100644
index 00000000..1126e346
--- /dev/null
+++ b/qt_ui/widgets/QLink4Widget.py
@@ -0,0 +1,95 @@
+from PySide2.QtCore import Signal
+from PySide2.QtWidgets import (
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QWidget,
+)
+
+from game.radio.radios import RadioFrequency, RadioRange, MHz, kHz
+from game.theater import NavalControlPoint
+from qt_ui.models import GameModel
+from qt_ui.windows.QRadioFrequencyDialog import QRadioFrequencyDialog
+
+
+class QLink4Widget(QWidget):
+
+ freq_changed = Signal(QWidget)
+
+ def __init__(self, cp: NavalControlPoint, game_model: GameModel) -> None:
+ super().__init__()
+
+ self.cp = cp
+ self.gm = game_model
+
+ columns = QHBoxLayout()
+ self.setLayout(columns)
+
+ self.freq = QLabel(self._get_label_text())
+ self.check_freq()
+ columns.addWidget(self.freq)
+ columns.addStretch()
+
+ self.set_freq_btn = QPushButton("Set LINK4")
+ self.set_freq_btn.setProperty("class", "comms")
+ self.set_freq_btn.setFixedWidth(100)
+ columns.addWidget(self.set_freq_btn)
+ self.set_freq_btn.clicked.connect(self.open_freq_dialog)
+
+ self.reset_freq_btn = QPushButton("Reset LINK4")
+ self.reset_freq_btn.setProperty("class", "btn-danger comms")
+ self.reset_freq_btn.setFixedWidth(100)
+ columns.addWidget(self.reset_freq_btn)
+ self.reset_freq_btn.clicked.connect(self.reset_freq)
+
+ def _get_label_text(self) -> str:
+ freq = "AUTO" if self.cp.link4 is None else self.cp.link4
+ return f"LINK4: {freq}"
+
+ def open_freq_dialog(self) -> None:
+ range = RadioRange(MHz(225), MHz(400), kHz(25))
+ self.frequency_dialog = QRadioFrequencyDialog(self, self.cp, range, link4=True)
+ self.frequency_dialog.accepted.connect(self.assign_frequency)
+ self.frequency_dialog.show()
+
+ def assign_frequency(self) -> None:
+ hz = round(self.frequency_dialog.frequency_input.value() * 10**6)
+ self._try_remove()
+ self.cp.link4 = RadioFrequency(hertz=hz)
+ self.gm.allocated_freqs.append(self.cp.link4)
+ self.freq.setText(self._get_label_text())
+ self.check_freq()
+ self.freq_changed.emit(self)
+
+ def reset_freq(self):
+ self._try_remove()
+ self.cp.link4 = None
+ self.freq.setText(self._get_label_text())
+ self._reset_color_and_tooltip()
+ self.freq_changed.emit(self)
+
+ def check_freq(self):
+ if self.cp.link4 is None:
+ return
+ if self.gm.allocated_freqs.count(self.cp.link4) > 1:
+ self.freq.setStyleSheet("color: orange")
+ self.freq.setToolTip(
+ "Double booked frequency, verify that this was your intention."
+ )
+ elif self.gm.allocated_freqs.count(self.cp.link4) == 1:
+ self._reset_color_and_tooltip()
+ if self.cp.link4 == MHz(243) or self.cp.link4 == MHz(121, 500):
+ self.freq.setStyleSheet("color: red")
+ self.freq.setToolTip(
+ "GUARD Freq. was assigned, verify that this was your intention."
+ )
+
+ def _reset_color_and_tooltip(self):
+ self.freq.setStyleSheet("color: white")
+ self.freq.setToolTip(None)
+
+ def _try_remove(self):
+ try:
+ self.gm.allocated_freqs.remove(self.cp.link4)
+ except ValueError:
+ pass
diff --git a/qt_ui/widgets/QTacanWidget.py b/qt_ui/widgets/QTacanWidget.py
new file mode 100644
index 00000000..bb227f44
--- /dev/null
+++ b/qt_ui/widgets/QTacanWidget.py
@@ -0,0 +1,90 @@
+from PySide2.QtWidgets import (
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QWidget,
+)
+
+from game.radio.TacanContainer import TacanContainer
+from game.radio.tacan import TacanChannel, TacanBand
+from qt_ui.models import GameModel
+from qt_ui.windows.QTacanDialog import QTacanDialog
+
+
+class QTacanWidget(QWidget):
+ def __init__(self, container: TacanContainer, game_model: GameModel) -> None:
+ super().__init__()
+
+ self.ct = container
+ self.gm = game_model
+
+ columns = QHBoxLayout()
+ self.setLayout(columns)
+
+ self.channel = QLabel(self._get_label_text())
+ self.check_channel()
+ columns.addWidget(self.channel)
+ columns.addStretch()
+
+ self.set_tacan_btn = QPushButton("Set TACAN")
+ self.set_tacan_btn.setProperty("class", "comms")
+ self.set_tacan_btn.setFixedWidth(100)
+ columns.addWidget(self.set_tacan_btn)
+ self.set_tacan_btn.clicked.connect(self.open_tacan_dialog)
+
+ self.reset_tacan_btn = QPushButton("Reset TACAN")
+ self.reset_tacan_btn.setProperty("class", "btn-danger comms")
+ self.reset_tacan_btn.setFixedWidth(100)
+ columns.addWidget(self.reset_tacan_btn)
+ self.reset_tacan_btn.clicked.connect(self.reset_tacan)
+
+ def _get_label_text(self) -> str:
+ c = "AUTO" if self.ct.tacan is None else self.ct.tacan
+ cs = self.ct.tcn_name
+ cs = "" if cs is None else f" ({cs})"
+ return f"TACAN: {c}{cs}"
+
+ def open_tacan_dialog(self) -> None:
+ self.tacan_dialog = QTacanDialog(self, self.ct)
+ self.tacan_dialog.accepted.connect(self.assign_tacan)
+ self.tacan_dialog.show()
+
+ def assign_tacan(self) -> None:
+ channel = self.tacan_dialog.tacan_input.value()
+ band = self.tacan_dialog.band_input.currentText()
+ band = TacanBand.X if band == "X" else TacanBand.Y
+ self._try_remove()
+ self.ct.tacan = TacanChannel(number=channel, band=band)
+ self.gm.allocated_tacan.append(self.ct.tacan)
+ if cs := self.tacan_dialog.callsign_input.text():
+ self.ct.tcn_name = cs.upper()
+ self.channel.setText(self._get_label_text())
+ self.check_channel()
+
+ def reset_tacan(self) -> None:
+ self._try_remove()
+ self.ct.tacan = None
+ self.ct.tcn_name = None
+ self.channel.setText(self._get_label_text())
+ self._reset_color_and_tooltip()
+
+ def check_channel(self) -> None:
+ if self.ct.tacan is None:
+ return
+ if self.gm.allocated_tacan.count(self.ct.tacan) > 1:
+ self.channel.setStyleSheet("color: orange")
+ self.channel.setToolTip(
+ "Double booked TACAN channel, verify that this was your intention."
+ )
+ elif self.gm.allocated_tacan.count(self.ct.tacan) == 1:
+ self._reset_color_and_tooltip()
+
+ def _reset_color_and_tooltip(self):
+ self.channel.setStyleSheet("color: white")
+ self.channel.setToolTip(None)
+
+ def _try_remove(self) -> None:
+ try:
+ self.gm.allocated_tacan.remove(self.ct.tacan)
+ except ValueError:
+ pass
diff --git a/qt_ui/windows/QICLSDialog.py b/qt_ui/windows/QICLSDialog.py
new file mode 100644
index 00000000..d69d0fe1
--- /dev/null
+++ b/qt_ui/windows/QICLSDialog.py
@@ -0,0 +1,54 @@
+from typing import Optional
+
+from PySide2.QtCore import Qt
+from PySide2.QtWidgets import (
+ QDialog,
+ QPushButton,
+ QLabel,
+ QHBoxLayout,
+ QSpinBox,
+ QLineEdit,
+)
+
+from game.radio.ICLSContainer import ICLSContainer
+from qt_ui.uiconstants import EVENT_ICONS
+
+
+class QICLSDialog(QDialog):
+ def __init__(self, parent=None, container: Optional[ICLSContainer] = None) -> None:
+ super().__init__(parent=parent)
+ self.container = container
+ self.setMinimumWidth(400)
+
+ # Make dialog modal to prevent background windows to close unexpectedly.
+ self.setModal(True)
+
+ self.setWindowTitle("Assign ICLS")
+ self.setWindowIcon(EVENT_ICONS["strike"])
+
+ layout = QHBoxLayout()
+
+ self.icls_label = QLabel("ICLS:")
+ self.icls_input = QSpinBox()
+ self.icls_input.setRange(1, 20)
+ self.icls_input.setSingleStep(1)
+ layout.addWidget(self.icls_label)
+ layout.addStretch()
+ layout.addWidget(self.icls_input)
+ self.callsign_input = QLineEdit()
+ self.callsign_input.setMaxLength(3)
+ self.callsign_input.setMaximumWidth(50)
+ layout.addWidget(self.callsign_input)
+ layout.addStretch()
+
+ self.create_button = QPushButton("Save")
+ self.create_button.clicked.connect(self.accept)
+ layout.addWidget(self.create_button, alignment=Qt.AlignRight)
+
+ self.setLayout(layout)
+
+ if container is not None:
+ if container.icls_name is not None:
+ self.callsign_input.setText(container.icls_name)
+ if container.icls_channel is not None:
+ self.icls_input.setValue(container.icls_channel)
diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py
index a69bd5a2..4b52f60b 100644
--- a/qt_ui/windows/QLiberationWindow.py
+++ b/qt_ui/windows/QLiberationWindow.py
@@ -404,6 +404,7 @@ class QLiberationWindow(QMainWindow):
self.info_panel.setGame(game)
self.sim_controller.set_game(game)
self.game_model.set(self.game)
+ self.game_model.init_comms_registry()
except AttributeError:
logging.exception("Incompatible save game")
QMessageBox.critical(
@@ -530,6 +531,7 @@ class QLiberationWindow(QMainWindow):
logging.info("On Debriefing")
self.debriefing = QDebriefingWindow(debrief)
self.debriefing.show()
+ self.game_model.init_comms_registry()
def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show()
diff --git a/qt_ui/windows/QRadioFrequencyDialog.py b/qt_ui/windows/QRadioFrequencyDialog.py
new file mode 100644
index 00000000..edcf3639
--- /dev/null
+++ b/qt_ui/windows/QRadioFrequencyDialog.py
@@ -0,0 +1,60 @@
+from typing import Optional
+
+from PySide2.QtCore import Qt, QLocale
+from PySide2.QtWidgets import (
+ QDialog,
+ QPushButton,
+ QLabel,
+ QHBoxLayout,
+ QDoubleSpinBox,
+)
+
+from game.radio.Link4Container import Link4Container
+from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
+from game.radio.radios import RadioRange, kHz, MHz
+from qt_ui.uiconstants import EVENT_ICONS
+
+
+class QRadioFrequencyDialog(QDialog):
+ def __init__(
+ self,
+ parent=None,
+ container: Optional[RadioFrequencyContainer] = None,
+ range: RadioRange = RadioRange(MHz(225), MHz(400), kHz(25)),
+ link4: bool = False,
+ ) -> None:
+ super().__init__(parent=parent)
+ self.container = container
+ self.setMinimumWidth(400)
+
+ # Make dialog modal to prevent background windows to close unexpectedly.
+ self.setModal(True)
+
+ self.setWindowTitle("Assign frequency")
+ self.setWindowIcon(EVENT_ICONS["strike"])
+
+ layout = QHBoxLayout()
+
+ self.frequency_label = QLabel("FREQ (MHz):")
+ self.frequency_input = QDoubleSpinBox()
+ self.frequency_input.setRange(
+ range.minimum.mhz, range.maximum.mhz - range.step.mhz
+ )
+ self.frequency_input.setSingleStep(range.step.mhz)
+ self.frequency_input.setDecimals(3)
+ self.frequency_input.setLocale(QLocale(QLocale.Language.English))
+ self.frequency_input.setValue(225.0)
+ layout.addWidget(self.frequency_label)
+ layout.addWidget(self.frequency_input)
+
+ self.create_button = QPushButton("Save")
+ self.create_button.clicked.connect(self.accept)
+ layout.addWidget(self.create_button, alignment=Qt.AlignRight)
+
+ self.setLayout(layout)
+
+ if link4 and isinstance(container, Link4Container):
+ if container.link4:
+ self.frequency_input.setValue(container.link4.mhz)
+ elif container.frequency:
+ self.frequency_input.setValue(container.frequency.mhz)
diff --git a/qt_ui/windows/QTacanDialog.py b/qt_ui/windows/QTacanDialog.py
new file mode 100644
index 00000000..b5042a0e
--- /dev/null
+++ b/qt_ui/windows/QTacanDialog.py
@@ -0,0 +1,59 @@
+from typing import Optional
+
+from PySide2.QtCore import Qt
+from PySide2.QtWidgets import (
+ QDialog,
+ QPushButton,
+ QLabel,
+ QHBoxLayout,
+ QSpinBox,
+ QComboBox,
+ QLineEdit,
+)
+
+from game.radio.TacanContainer import TacanContainer
+from qt_ui.uiconstants import EVENT_ICONS
+
+
+class QTacanDialog(QDialog):
+ def __init__(self, parent=None, container: Optional[TacanContainer] = None) -> None:
+ super().__init__(parent=parent)
+ self.container = container
+ self.setMinimumWidth(400)
+
+ # Make dialog modal to prevent background windows to close unexpectedly.
+ self.setModal(True)
+
+ self.setWindowTitle("Assign TACAN")
+ self.setWindowIcon(EVENT_ICONS["strike"])
+
+ layout = QHBoxLayout()
+
+ self.tacan_label = QLabel("TACAN:")
+ self.tacan_input = QSpinBox()
+ self.tacan_input.setRange(1, 126)
+ self.tacan_input.setSingleStep(1)
+ layout.addWidget(self.tacan_label)
+ layout.addStretch()
+ layout.addWidget(self.tacan_input)
+ self.band_input = QComboBox()
+ self.band_input.addItems(["X", "Y"])
+ layout.addWidget(self.band_input)
+ self.callsign_input = QLineEdit()
+ self.callsign_input.setMaxLength(3)
+ self.callsign_input.setMaximumWidth(50)
+ layout.addWidget(self.callsign_input)
+ layout.addStretch()
+
+ self.create_button = QPushButton("Save")
+ self.create_button.clicked.connect(self.accept)
+ layout.addWidget(self.create_button, alignment=Qt.AlignRight)
+
+ self.setLayout(layout)
+
+ if container is not None:
+ if container.tcn_name is not None:
+ self.callsign_input.setText(container.tcn_name)
+ if container.tacan is not None:
+ self.tacan_input.setValue(container.tacan.number)
+ self.band_input.setCurrentText(container.tacan.band.value)
diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py
index 6932c6b8..73af5c9b 100644
--- a/qt_ui/windows/basemenu/QBaseMenu2.py
+++ b/qt_ui/windows/basemenu/QBaseMenu2.py
@@ -8,11 +8,15 @@ from PySide2.QtWidgets import (
QPushButton,
QVBoxLayout,
QWidget,
+ QGridLayout,
)
from game import Game
from game.ato.flighttype import FlightType
from game.config import RUNWAY_REPAIR_COST
+from game.radio.ICLSContainer import ICLSContainer
+from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
+from game.radio.TacanContainer import TacanContainer
from game.server import EventStream
from game.sim import GameUpdateEvents
from game.theater import (
@@ -20,10 +24,15 @@ from game.theater import (
ControlPoint,
ControlPointType,
FREE_FRONTLINE_UNIT_SUPPLY,
+ NavalControlPoint,
)
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.uiconstants import EVENT_ICONS
+from qt_ui.widgets.QFrequencyWidget import QFrequencyWidget
+from qt_ui.widgets.QICLSWidget import QICLSWidget
+from qt_ui.widgets.QLink4Widget import QLink4Widget
+from qt_ui.widgets.QTacanWidget import QTacanWidget
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.basemenu.NewUnitTransferDialog import NewUnitTransferDialog
from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs
@@ -60,13 +69,49 @@ class QBaseMenu2(QDialog):
pixmap = QPixmap(self.get_base_image())
header.setPixmap(pixmap)
+ cp_settings = QGridLayout()
+ top_layout.addLayout(cp_settings)
+
title = QLabel("" + self.cp.name + "")
title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title")
+ cp_settings.addWidget(title, 0, 0, 1, 2)
+ cp_settings.setHorizontalSpacing(20)
+
+ counter = 2
+
+ self.freq_widget = None
+ self.link4_widget = None
+
+ is_friendly = cp.is_friendly(True)
+ if is_friendly and isinstance(cp, RadioFrequencyContainer):
+ self.freq_widget = QFrequencyWidget(cp, self.game_model)
+ cp_settings.addWidget(self.freq_widget, counter // 2, counter % 2)
+ counter += 1
+
+ if is_friendly and isinstance(cp, TacanContainer):
+ self.tacan_widget = QTacanWidget(cp, self.game_model)
+ cp_settings.addWidget(self.tacan_widget, counter // 2, counter % 2)
+ counter += 1
+
+ if is_friendly and isinstance(cp, ICLSContainer):
+ self.icls_widget = QICLSWidget(cp, self.game_model)
+ cp_settings.addWidget(self.icls_widget, counter // 2, counter % 2)
+ counter += 1
+
+ if is_friendly and isinstance(cp, NavalControlPoint):
+ self.link4_widget = QLink4Widget(cp, self.game_model)
+ cp_settings.addWidget(self.link4_widget, counter // 2, counter % 2)
+ counter += 1
+
+ if self.freq_widget and self.link4_widget:
+ # link them so on change they check freq
+ self.freq_widget.freq_changed.connect(self.link4_widget.check_freq)
+ self.link4_widget.freq_changed.connect(self.freq_widget.check_freq)
+
self.intel_summary = QLabel()
self.intel_summary.setToolTip(self.generate_intel_tooltip())
self.update_intel_summary()
- top_layout.addWidget(title)
top_layout.addWidget(self.intel_summary)
top_layout.setAlignment(Qt.AlignTop)
@@ -127,6 +172,7 @@ class QBaseMenu2(QDialog):
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
state = self.game_model.game.check_win_loss()
GameUpdateSignal.get_instance().gameStateChanged(state)
+ self.close()
@property
def has_transfer_destinations(self) -> bool:
diff --git a/qt_ui/windows/mission/QEditFlightDialog.py b/qt_ui/windows/mission/QEditFlightDialog.py
index 40d43cf7..0f94cc42 100644
--- a/qt_ui/windows/mission/QEditFlightDialog.py
+++ b/qt_ui/windows/mission/QEditFlightDialog.py
@@ -32,7 +32,7 @@ class QEditFlightDialog(QDialog):
layout = QVBoxLayout()
- self.flight_planner = QFlightPlanner(package_model, flight, game_model.game)
+ self.flight_planner = QFlightPlanner(package_model, flight, game_model)
layout.addWidget(self.flight_planner)
self.setLayout(layout)
diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py
index de12dafc..7b0c8948 100644
--- a/qt_ui/windows/mission/QPackageDialog.py
+++ b/qt_ui/windows/mission/QPackageDialog.py
@@ -26,9 +26,10 @@ from game.sim import GameUpdateEvents
from game.theater.missiontarget import MissionTarget
from qt_ui.models import AtoModel, GameModel, PackageModel
from qt_ui.uiconstants import EVENT_ICONS
+from qt_ui.widgets.QFrequencyWidget import QFrequencyWidget
from qt_ui.widgets.ato import QFlightList
+from qt_ui.windows.QRadioFrequencyDialog import QRadioFrequencyDialog
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
-from qt_ui.windows.mission.package.QPackageFrequency import QPackageFrequency
class QPackageDialog(QDialog):
@@ -70,22 +71,9 @@ class QPackageDialog(QDialog):
package_type_row.addWidget(self.package_type_text)
self.package_type_column.addLayout(package_type_row)
- # TODO: make freq red if used by another package
- package_freq_row = QHBoxLayout()
- self.package_freq_label = QLabel("Package FREQ:")
- freq = (
- self.package_model.package.frequency.mhz
- if self.package_model.package.frequency is not None
- else ""
- )
- self.package_freq_text = QLabel(f"{freq}")
- package_freq_row.addWidget(self.package_freq_label)
- package_freq_row.addWidget(self.package_freq_text)
- self.package_type_column.addLayout(package_freq_row)
-
self.summary_row.addStretch(1)
- self.package_name_column = QVBoxLayout()
+ self.package_name_column = QHBoxLayout()
self.summary_row.addLayout(self.package_name_column)
self.package_name_label = QLabel("Package Name:")
self.package_name_label.setAlignment(Qt.AlignCenter)
@@ -145,16 +133,17 @@ class QPackageDialog(QDialog):
self.delete_flight_button.setEnabled(model.rowCount() > 0)
self.button_layout.addWidget(self.delete_flight_button)
- self.open_radio_button = QPushButton("Set Package FREQ")
- self.open_radio_button.clicked.connect(self.on_open_radio)
- self.button_layout.addWidget(self.open_radio_button)
+ self.button_layout.addStretch()
- self.package_model.tot_changed.connect(self.update_tot)
+ self.freq_widget = QFrequencyWidget(self.package_model.package, game_model)
+ self.button_layout.addWidget(self.freq_widget)
self.button_layout.addStretch()
self.setLayout(self.layout)
+ self.package_model.tot_changed.connect(self.update_tot)
+
self.accepted.connect(self.on_save)
self.finished.connect(self.on_close)
self.rejected.connect(self.on_cancel)
@@ -238,8 +227,8 @@ class QPackageDialog(QDialog):
self.package_model.package.custom_name = self.package_name_text.text()
def on_open_radio(self) -> None:
- self.package_frequency_dialog = QPackageFrequency(
- self.game, self.package_model.package, parent=self.window()
+ self.package_frequency_dialog = QRadioFrequencyDialog(
+ parent=self.window(), container=self.package_model.package
)
self.package_frequency_dialog.accepted.connect(self.assign_frequency)
self.package_frequency_dialog.show()
@@ -251,8 +240,11 @@ class QPackageDialog(QDialog):
def on_package_changed(self):
self.package_type_text.setText(self.package_model.description)
- if (freq := self.package_model.package.frequency) is not None:
- self.package_freq_text.setText(f"{freq.mhz} MHz")
+ self.freq_widget.check_freq()
+
+ def on_reset_radio(self):
+ self.package_model.package.frequency = None
+ self.package_freq_text.setText("AUTO")
class QNewPackageDialog(QPackageDialog):
diff --git a/qt_ui/windows/mission/flight/QFlightPlanner.py b/qt_ui/windows/mission/flight/QFlightPlanner.py
index ab86f052..69178c2d 100644
--- a/qt_ui/windows/mission/flight/QFlightPlanner.py
+++ b/qt_ui/windows/mission/flight/QFlightPlanner.py
@@ -1,8 +1,7 @@
from PySide2.QtWidgets import QTabWidget
-from game import Game
from game.ato.flight import Flight
-from qt_ui.models import PackageModel
+from qt_ui.models import PackageModel, GameModel
from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import QFlightPayloadTab
from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import (
QGeneralFlightSettingsTab,
@@ -11,14 +10,12 @@ from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import QFlightWay
class QFlightPlanner(QTabWidget):
- def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
+ def __init__(self, package_model: PackageModel, flight: Flight, gm: GameModel):
super().__init__()
- self.general_settings_tab = QGeneralFlightSettingsTab(
- game, package_model, flight
- )
- self.payload_tab = QFlightPayloadTab(flight, game)
- self.waypoint_tab = QFlightWaypointTab(game, package_model.package, flight)
+ self.general_settings_tab = QGeneralFlightSettingsTab(gm, package_model, flight)
+ self.payload_tab = QFlightPayloadTab(flight, gm.game)
+ self.waypoint_tab = QFlightWaypointTab(gm.game, package_model.package, flight)
self.waypoint_tab.loadout_changed.connect(self.payload_tab.reload_from_flight)
self.addTab(self.general_settings_tab, "General Flight settings")
self.addTab(self.payload_tab, "Payload")
diff --git a/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py b/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py
index 844ad801..b9177383 100644
--- a/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py
+++ b/qt_ui/windows/mission/flight/payload/QFlightPayloadTab.py
@@ -4,6 +4,8 @@ from PySide2.QtWidgets import (
QFrame,
QLabel,
QVBoxLayout,
+ QScrollArea,
+ QWidget,
)
from game import Game
@@ -42,7 +44,12 @@ class QFlightPayloadTab(QFrame):
docsText.setAlignment(Qt.AlignCenter)
docsText.setOpenExternalLinks(True)
- layout.addLayout(PropertyEditor(self.flight))
+ self.scroll_area = QScrollArea()
+ self.property_editor = QWidget()
+ self.property_editor.setLayout(PropertyEditor(self.flight))
+ self.scroll_area.setWidget(self.property_editor)
+
+ layout.addWidget(self.scroll_area)
self.loadout_selector = DcsLoadoutSelector(flight)
self.loadout_selector.currentIndexChanged.connect(self.on_new_loadout)
layout.addWidget(self.loadout_selector)
diff --git a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py
index 0b70af76..c0048984 100644
--- a/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py
+++ b/qt_ui/windows/mission/flight/payload/QLoadoutEditor.py
@@ -30,7 +30,6 @@ class QLoadoutEditor(QGroupBox):
layout.addWidget(QPylonEditor(game, flight, pylon), i, 1)
vbox.addLayout(layout)
- vbox.addStretch()
self.setLayout(vbox)
for i in self.findChildren(QPylonEditor):
diff --git a/qt_ui/windows/mission/flight/settings/QCommsEditor.py b/qt_ui/windows/mission/flight/settings/QCommsEditor.py
new file mode 100644
index 00000000..8c738561
--- /dev/null
+++ b/qt_ui/windows/mission/flight/settings/QCommsEditor.py
@@ -0,0 +1,25 @@
+from PySide2.QtWidgets import QGroupBox, QVBoxLayout
+
+from game.ato import Flight, FlightType
+from qt_ui.models import GameModel
+from qt_ui.widgets.QFrequencyWidget import QFrequencyWidget
+from qt_ui.widgets.QTacanWidget import QTacanWidget
+
+
+class QCommsEditor(QGroupBox):
+ def __init__(self, flight: Flight, game: GameModel):
+ title = "Intra-Flight Frequency"
+
+ layout = QVBoxLayout()
+
+ is_refuel = flight.flight_type == FlightType.REFUELING
+ has_tacan = flight.unit_type.dcs_unit_type.tacan
+
+ layout.addWidget(QFrequencyWidget(flight, game))
+ if is_refuel and has_tacan:
+ layout.addWidget(QTacanWidget(flight, game))
+ title = title + " / TACAN"
+ super(QCommsEditor, self).__init__(title)
+ self.flight = flight
+
+ self.setLayout(layout)
diff --git a/qt_ui/windows/mission/flight/settings/QCustomName.py b/qt_ui/windows/mission/flight/settings/QCustomName.py
index 1258a4b2..bd614d0c 100644
--- a/qt_ui/windows/mission/flight/settings/QCustomName.py
+++ b/qt_ui/windows/mission/flight/settings/QCustomName.py
@@ -1,6 +1,6 @@
from typing import Optional
-from PySide2.QtWidgets import QGroupBox, QVBoxLayout, QLineEdit, QLabel, QMessageBox
+from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLineEdit, QLabel, QMessageBox
from game.ato.flight import Flight
@@ -10,7 +10,7 @@ class QFlightCustomName(QGroupBox):
self.flight = flight
- self.layout = QVBoxLayout()
+ self.layout = QHBoxLayout()
self.custom_name_label = QLabel(f"Custom Name:")
self.custom_name_input = QLineEdit(flight.custom_name)
self.custom_name_input.textChanged.connect(self.on_change)
diff --git a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py
index 0c9070a5..8cb30d4e 100644
--- a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py
+++ b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py
@@ -3,10 +3,11 @@ from PySide2.QtWidgets import QFrame, QGridLayout, QVBoxLayout
from game import Game
from game.ato.flight import Flight
-from qt_ui.models import PackageModel
+from qt_ui.models import PackageModel, GameModel
from qt_ui.windows.mission.flight.settings.FlightAirfieldDisplay import (
FlightAirfieldDisplay,
)
+from qt_ui.windows.mission.flight.settings.QCommsEditor import QCommsEditor
from qt_ui.windows.mission.flight.settings.QCustomName import QFlightCustomName
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import QFlightSlotEditor
from qt_ui.windows.mission.flight.settings.QFlightStartType import QFlightStartType
@@ -18,16 +19,23 @@ from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import (
class QGeneralFlightSettingsTab(QFrame):
on_flight_settings_changed = Signal()
- def __init__(self, game: Game, package_model: PackageModel, flight: Flight):
+ def __init__(self, game: GameModel, package_model: PackageModel, flight: Flight):
super().__init__()
+ widgets = [
+ QFlightTypeTaskInfo(flight),
+ QCommsEditor(flight, game),
+ FlightAirfieldDisplay(game.game, package_model, flight),
+ QFlightSlotEditor(package_model, flight, game.game),
+ QFlightStartType(package_model, flight),
+ QFlightCustomName(flight),
+ ]
layout = QGridLayout()
- layout.addWidget(QFlightTypeTaskInfo(flight), 0, 0)
- layout.addWidget(FlightAirfieldDisplay(game, package_model, flight), 1, 0)
- layout.addWidget(QFlightSlotEditor(package_model, flight, game), 2, 0)
- layout.addWidget(QFlightStartType(package_model, flight), 3, 0)
- layout.addWidget(QFlightCustomName(flight), 4, 0)
+ row = 0
+ for w in widgets:
+ layout.addWidget(w, row, 0)
+ row += 1
vstretch = QVBoxLayout()
vstretch.addStretch()
- layout.addLayout(vstretch, 5, 0)
+ layout.addLayout(vstretch, row, 0)
self.setLayout(layout)
diff --git a/qt_ui/windows/mission/package/QPackageFrequency.py b/qt_ui/windows/mission/package/QPackageFrequency.py
deleted file mode 100644
index 7a97f319..00000000
--- a/qt_ui/windows/mission/package/QPackageFrequency.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from typing import Optional, Type
-
-from PySide2.QtCore import Qt
-from PySide2.QtWidgets import (
- QDialog,
- QPushButton,
- QVBoxLayout,
- QLabel,
- QHBoxLayout,
- QDoubleSpinBox,
-)
-
-from game import Game
-from game.ato.package import Package
-from game.radio.radios import RadioRegistry
-from qt_ui.uiconstants import EVENT_ICONS
-
-
-class QPackageFrequency(QDialog):
- def __init__(self, game: Game, package: Package, parent=None) -> None:
- super().__init__(parent=parent)
- self.setMinimumWidth(400)
-
- self.game = game
- self.package = package
-
- # Make dialog modal to prevent background windows to close unexpectedly.
- self.setModal(True)
-
- self.setWindowTitle("Assign frequency")
- self.setWindowIcon(EVENT_ICONS["strike"])
-
- layout = QHBoxLayout()
-
- self.frequency_label = QLabel("FREQ (Mhz):")
- self.frequency_input = QDoubleSpinBox()
- self.frequency_input.setRange(225, 399.975)
- self.frequency_input.setSingleStep(0.025)
- self.frequency_input.setDecimals(3)
- layout.addWidget(self.frequency_label)
- layout.addWidget(self.frequency_input)
-
- self.create_button = QPushButton("Save")
- self.create_button.clicked.connect(self.accept)
- layout.addWidget(self.create_button, alignment=Qt.AlignRight)
-
- self.setLayout(layout)
diff --git a/requirements.txt b/requirements.txt
index 56d4be3b..6c0ccecc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -30,7 +30,7 @@ platformdirs==2.5.4
pluggy==1.0.0
pre-commit==2.20.0
pydantic==1.10.2
--e git+https://github.com/dcs-retribution/pydcs@14ba81f5d61baa9c181c3008d6baa631f2a1587c#egg=pydcs
+-e git+https://github.com/dcs-retribution/pydcs@81b11734710c050b1c162b269668945cfb9fd2f0#egg=pydcs
pyinstaller==5.6.2
pyinstaller-hooks-contrib==2022.13
pyparsing==3.0.9
diff --git a/resources/stylesheets/style-dcs.css b/resources/stylesheets/style-dcs.css
index 2d0e0ccf..601dd897 100644
--- a/resources/stylesheets/style-dcs.css
+++ b/resources/stylesheets/style-dcs.css
@@ -208,7 +208,6 @@ QPushButton[style="btn-danger"]:hover{
background-color:#D84545;
}
-
QPushButton[style="btn-accept"] {
background-color:#82A466;
color: white;
@@ -510,7 +509,11 @@ QLineEdit{
border:1px solid #3B4656;
background: #465C74;
color: #fff;
- margin-bottom:10px;
+}
+
+.tacan-edit {
+ background: #1D2731;
+ text-transform: uppercase;
}
@@ -642,4 +645,22 @@ QCalendarWidget QTableView{
border-width: 2px;
background-color:lightgrey;
border: 1px solid black;
-}
\ No newline at end of file
+}
+
+/* Plain old classes */
+
+.btn-danger {
+ background-color:#9E3232;
+ color: white;
+ padding: 6px;
+ border-radius:2px;
+ border: 1px solid #9E3232;
+}
+
+.btn-danger:hover {
+ background-color:#D84545;
+}
+
+.comms {
+ padding: 2px;
+}