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:
Raffson 2023-01-14 15:42:13 +01:00
parent ddb9d6968b
commit 88f984b0a8
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
37 changed files with 992 additions and 177 deletions

View File

@ -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

View File

@ -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

View File

@ -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__(

View File

@ -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")

View File

@ -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)

View File

@ -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),

View File

@ -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(

View File

@ -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]

View File

@ -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,

View File

@ -0,0 +1,6 @@
from typing import Optional
class ICLSContainer:
icls_channel: Optional[int] = None
icls_name: Optional[str] = None

View 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

View File

@ -0,0 +1,7 @@
from typing import Optional
from game.radio.radios import RadioFrequency
class RadioFrequencyContainer:
frequency: Optional[RadioFrequency] = None

View 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

View File

@ -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

View File

@ -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)))

View File

@ -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
):

View File

@ -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

View 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

View 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

View 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

View 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

View 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)

View File

@ -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()

View 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)

View 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)

View File

@ -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:

View File

@ -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)

View File

@ -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):

View File

@ -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")

View File

@ -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)

View File

@ -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):

View 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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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;
}