mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Configurable RF/TCN/ICLS/LINK4 with UI feedback
Resolves #70 Freq/Channel will turn orange when double booked. Freq will turn red if GUARD freq was assigned.
This commit is contained in:
parent
ddb9d6968b
commit
88f984b0a8
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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__(
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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,
|
||||
|
||||
6
game/radio/ICLSContainer.py
Normal file
6
game/radio/ICLSContainer.py
Normal file
@ -0,0 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ICLSContainer:
|
||||
icls_channel: Optional[int] = None
|
||||
icls_name: Optional[str] = None
|
||||
8
game/radio/Link4Container.py
Normal file
8
game/radio/Link4Container.py
Normal file
@ -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
|
||||
7
game/radio/RadioFrequencyContainer.py
Normal file
7
game/radio/RadioFrequencyContainer.py
Normal file
@ -0,0 +1,7 @@
|
||||
from typing import Optional
|
||||
|
||||
from game.radio.radios import RadioFrequency
|
||||
|
||||
|
||||
class RadioFrequencyContainer:
|
||||
frequency: Optional[RadioFrequency] = None
|
||||
8
game/radio/TacanContainer.py
Normal file
8
game/radio/TacanContainer.py
Normal file
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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)))
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
):
|
||||
|
||||
@ -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
|
||||
|
||||
101
qt_ui/widgets/QFrequencyWidget.py
Normal file
101
qt_ui/widgets/QFrequencyWidget.py
Normal file
@ -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"<b>FREQ: {freq}</b>"
|
||||
|
||||
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
|
||||
86
qt_ui/widgets/QICLSWidget.py
Normal file
86
qt_ui/widgets/QICLSWidget.py
Normal file
@ -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"<b>ICLS: {c}{cs}</b>"
|
||||
|
||||
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
|
||||
95
qt_ui/widgets/QLink4Widget.py
Normal file
95
qt_ui/widgets/QLink4Widget.py
Normal file
@ -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"<b>LINK4: {freq}</b>"
|
||||
|
||||
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
|
||||
90
qt_ui/widgets/QTacanWidget.py
Normal file
90
qt_ui/widgets/QTacanWidget.py
Normal file
@ -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"<b>TACAN: {c}{cs}</b>"
|
||||
|
||||
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
|
||||
54
qt_ui/windows/QICLSDialog.py
Normal file
54
qt_ui/windows/QICLSDialog.py
Normal file
@ -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)
|
||||
@ -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()
|
||||
|
||||
60
qt_ui/windows/QRadioFrequencyDialog.py
Normal file
60
qt_ui/windows/QRadioFrequencyDialog.py
Normal file
@ -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)
|
||||
59
qt_ui/windows/QTacanDialog.py
Normal file
59
qt_ui/windows/QTacanDialog.py
Normal file
@ -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)
|
||||
@ -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("<b>" + self.cp.name + "</b>")
|
||||
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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
25
qt_ui/windows/mission/flight/settings/QCommsEditor.py
Normal file
25
qt_ui/windows/mission/flight/settings/QCommsEditor.py
Normal file
@ -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)
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user