Merge pull request #149 from Khopa/develop

2.1.1
This commit is contained in:
C. Perreau 2020-09-25 01:06:09 +02:00 committed by GitHub
commit 7eea328706
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 7095 additions and 475 deletions

4
.gitignore vendored
View File

@ -12,7 +12,9 @@ tests/**
# User-specific stuff
.idea/
liberation_preferences.json
/kneeboards
/liberation_preferences.json
/state.json
logs/liberation.log

19
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Main",
"type": "python",
"request": "launch",
"program": "qt_ui\\main.py",
"console": "integratedTerminal",
"env": {
"PYTHONPATH": ".;./pydcs"
},
"preLaunchTask": "Prepare Environment"
}
]
}

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"python.pythonPath": "g:\\python\\dcs_liberation\\venv\\Scripts\\python.exe",
"vsintellicode.python.completionsEnabled": true
}

35
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,35 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Prepare Environment",
"type": "shell",
"isBackground": false,
"problemMatcher": [],
"command": "powershell",
"args": [
"-Command",
"& {if (-not (Test-Path ${workspaceFolder}\\venv)) { python -m venv ${workspaceFolder}\\venv; . ${workspaceFolder}\\venv\\scripts\\activate.ps1; pip install -r requirements.txt; Copy-Item ${workspaceFolder}\\venv\\Lib\\site-packages\\shiboken2\\shiboken2.abi3.dll ${workspaceFolder}\\venv\\Lib\\site-packages\\PySide2 } }",
],
"group": "build",
"options": {
"env": {
"PYTHONPATH": ".;./pydcs"
}
},
"presentation": {
"echo": true,
"reveal": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
},
"runOptions": {
"runOn": "folderOpen"
}
}
]
}

View File

@ -2,8 +2,23 @@
## Features/Improvements :
* **[Other]** Added an installer option (thanks to contributor parithon)
* **[Cheat Menu]** Added possibility to replace destroyed SAM and base defenses units for the player (Click on a SAM site to fix it)
* **[Cheat Menu]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations
* **[Kneeboards]** Generate mission kneeboards for player flights. Kneeboards include
airfield/carrier information (ATC frequencies, ILS, TACAN, and runway
assignments), assigned radio channels, waypoint lists, and AWACS/JTAC/tanker
information.
* **[Radios]** Allocate separate intra-flight channels for most aircraft to reduce global
chatter.
* **[Radios]** Configure radio channel presets for most aircraft. Currently supported are:
* AJS37
* AV-8B
* F-14B
* F-16C
* F/A-18C
* JF-17
* M-2000C
* **[Base Menu]** Added possibility to repair destroyed SAM and base defenses units for the player (Click on a SAM site to fix it)
* **[Base Menu]** Added possibility to buy/sell/replace SAM units
* **[Map]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations
* **[Units/Factions]** Added F-16C to USA 1990
* **[Units/Factions]** Added MQ-9 Reaper as CAS unit for USA 2005
* **[Units/Factions]** Added Mig-21, Mig-23, SA-342L to Syria 2011
@ -17,6 +32,7 @@
* **[Mission Generator]** AH-1W was not used by AI to generate CAS mission by default
* **[Mission Generator]** Fixed F-16C targeting pod not being added to payload
* **[Mission Generator]** AH-64A and AH-64D payloads fix.
* **[Units/Factions]** China will use KJ-2000 as awacs instead of A-50
# 2.1.0

View File

@ -1,5 +1,5 @@
import inspect
from pydcs import dcs
import dcs
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa']

View File

@ -223,13 +223,16 @@ PRICES = {
KC130: 25,
A_50: 50,
KJ_2000: 50,
E_3A: 50,
C_130: 25,
# WW2
P_51D_30_NA: 18,
P_51D: 16,
P_47D_30: 18,
P_47D_30: 17,
P_47D_30bl1: 16,
P_47D_40: 18,
B_17G: 30,
# Drones
@ -337,15 +340,15 @@ PRICES = {
AirDefence.SAM_SA_11_Buk_LN_9A310M1: 30,
AirDefence.SAM_SA_8_Osa_9A33: 28,
AirDefence.SAM_SA_15_Tor_9A331: 40,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 24,
AirDefence.SAM_SA_9_Strela_1_9P31: 16,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 16,
AirDefence.SAM_SA_9_Strela_1_9P31: 12,
AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25,
AirDefence.SAM_SA_8_Osa_LD_9T217: 22,
AirDefence.SAM_Patriot_AMG_AN_MRC_137: 35,
AirDefence.SAM_Patriot_ECS_AN_MSQ_104: 30,
AirDefence.SPAAA_Gepard: 24,
AirDefence.SAM_Hawk_PCP: 14,
AirDefence.AAA_Vulcan_M163: 12,
AirDefence.AAA_Vulcan_M163: 10,
AirDefence.SAM_Hawk_LN_M192: 8,
AirDefence.SAM_Chaparral_M48: 16,
AirDefence.SAM_Linebacker_M6: 18,
@ -358,7 +361,7 @@ PRICES = {
AirDefence.Stinger_MANPADS: 6,
AirDefence.SAM_Stinger_comm_dsr: 4,
AirDefence.SAM_Stinger_comm: 4,
AirDefence.SPAAA_ZSU_23_4_Shilka: 12,
AirDefence.SPAAA_ZSU_23_4_Shilka: 10,
AirDefence.AAA_ZU_23_Closed: 6,
AirDefence.AAA_ZU_23_Emplacement: 6,
AirDefence.AAA_ZU_23_on_Ural_375: 8,
@ -387,19 +390,19 @@ PRICES = {
AirDefence.SAM_SA_2_LN_SM_90: 8,
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song: 12,
AirDefence.Rapier_FSA_Launcher: 6,
AirDefence.Rapier_FSA_Optical_Tracker: 12,
AirDefence.Rapier_FSA_Blindfire_Tracker: 16,
AirDefence.Rapier_FSA_Optical_Tracker: 6,
AirDefence.Rapier_FSA_Blindfire_Tracker: 8,
AirDefence.HQ_7_Self_Propelled_LN: 20,
AirDefence.HQ_7_Self_Propelled_STR: 24,
AirDefence.AAA_8_8cm_Flak_18: 6,
AirDefence.AAA_Flak_38: 6,
AirDefence.AAA_8_8cm_Flak_36: 8,
AirDefence.AAA_8_8cm_Flak_37: 10,
AirDefence.AAA_8_8cm_Flak_37: 9,
AirDefence.AAA_Flak_Vierling_38:6,
AirDefence.AAA_Kdo_G_40: 8,
AirDefence.Flak_Searchlight_37: 4,
AirDefence.Maschinensatz_33: 10,
AirDefence.AAA_8_8cm_Flak_41: 12,
AirDefence.AAA_8_8cm_Flak_41: 10,
AirDefence.AAA_Bofors_40mm: 8,
# FRENCH PACK MOD
@ -519,6 +522,8 @@ UNIT_BY_TASK = {
MiG_27K,
A_20G,
P_47D_30,
P_47D_30bl1,
P_47D_40,
Ju_88A4,
B_17G,
MB_339PAN,
@ -542,7 +547,7 @@ UNIT_BY_TASK = {
KC130,
S_3B_Tanker,
],
AWACS: [E_3A, A_50, ],
AWACS: [E_3A, A_50, KJ_2000],
PinpointStrike: [
Armor.APC_MTLB,
Armor.APC_MTLB,
@ -993,6 +998,8 @@ PLANE_PAYLOAD_OVERRIDES = {
Su_17M4: COMMON_OVERRIDE,
F_4E: COMMON_OVERRIDE,
P_47D_30:COMMON_OVERRIDE,
P_47D_30bl1:COMMON_OVERRIDE,
P_47D_40:COMMON_OVERRIDE,
B_17G: COMMON_OVERRIDE,
P_51D: COMMON_OVERRIDE,
P_51D_30_NA: COMMON_OVERRIDE,

View File

@ -20,7 +20,7 @@ China_2010 = {
An_30M,
Yak_40,
A_50,
KJ_2000,
Mi_8MT,
Mi_28N,

View File

@ -11,7 +11,7 @@ Sweden_1990 = {
UH_1H,
AirDefence.SAM_Hawk_LN_M192,
AirDefence.SAM_Hawk_PCP,
Armor.IFV_MCV_80, # Standing as Strf 90
Armor.MBT_Leopard_2,

View File

@ -36,6 +36,4 @@ class FrontlineAttackOperation(Operation):
def generate(self):
self.briefinggen.title = "Frontline CAS"
self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu."
self.briefinggen.append_waypoint("CAS AREA IP")
self.briefinggen.append_waypoint("CAS AREA EGRESS")
super(FrontlineAttackOperation, self).generate()

View File

@ -1,12 +1,15 @@
from dcs.countries import country_dict
from dcs.lua.parse import loads
from dcs.terrain import Terrain
from typing import Set
from gen import *
from gen.airfields import AIRFIELD_DATA
from gen.beacons import load_beacons_for_terrain
from gen.radios import RadioRegistry
from gen.tacan import TacanRegistry
from dcs.countries import country_dict
from dcs.lua.parse import loads
from dcs.terrain.terrain import Terrain
from userdata.debriefing import *
TANKER_CALLSIGNS = ["Texaco", "Arco", "Shell"]
class Operation:
attackers_starting_position = None # type: db.StartingPosition
@ -25,6 +28,8 @@ class Operation:
groundobjectgen = None # type: GroundObjectsGenerator
briefinggen = None # type: BriefingGenerator
forcedoptionsgen = None # type: ForcedOptionsGenerator
radio_registry: Optional[RadioRegistry] = None
tacan_registry: Optional[TacanRegistry] = None
environment_settings = None
trigger_radius = TRIGGER_RADIUS_MEDIUM
@ -63,13 +68,25 @@ class Operation:
def initialize(self, mission: Mission, conflict: Conflict):
self.current_mission = mission
self.conflict = conflict
self.airgen = AircraftConflictGenerator(mission, conflict, self.game.settings, self.game)
self.airsupportgen = AirSupportConflictGenerator(mission, conflict, self.game)
self.radio_registry = RadioRegistry()
self.tacan_registry = TacanRegistry()
self.airgen = AircraftConflictGenerator(
mission, conflict, self.game.settings, self.game,
self.radio_registry)
self.airsupportgen = AirSupportConflictGenerator(
mission, conflict, self.game, self.radio_registry,
self.tacan_registry)
self.triggersgen = TriggersGenerator(mission, conflict, self.game)
self.visualgen = VisualGenerator(mission, conflict, self.game)
self.envgen = EnviromentGenerator(mission, conflict, self.game)
self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game)
self.groundobjectgen = GroundObjectsGenerator(mission, conflict, self.game)
self.groundobjectgen = GroundObjectsGenerator(
mission,
conflict,
self.game,
self.radio_registry,
self.tacan_registry
)
self.briefinggen = BriefingGenerator(mission, conflict, self.game)
def prepare(self, terrain: Terrain, is_quick: bool):
@ -110,6 +127,30 @@ class Operation:
self.defenders_starting_position = self.to_cp.at
def generate(self):
# Dedup beacon/radio frequencies, since some maps have some frequencies
# used multiple times.
beacons = load_beacons_for_terrain(self.game.theater.terrain.name)
unique_map_frequencies: Set[RadioFrequency] = set()
for beacon in beacons:
unique_map_frequencies.add(beacon.frequency)
if beacon.is_tacan:
if beacon.channel is None:
logging.error(
f"TACAN beacon has no channel: {beacon.callsign}")
else:
self.tacan_registry.reserve(beacon.tacan_channel)
for airfield, data in AIRFIELD_DATA.items():
if data.theater == self.game.theater.terrain.name:
unique_map_frequencies.add(data.atc.hf)
unique_map_frequencies.add(data.atc.vhf_fm)
unique_map_frequencies.add(data.atc.vhf_am)
unique_map_frequencies.add(data.atc.uhf)
# No need to reserve ILS or TACAN because those are in the
# beacon list.
for frequency in unique_map_frequencies:
self.radio_registry.reserve(frequency)
# Generate meteo
if self.environment_settings is None:
@ -151,10 +192,15 @@ class Operation:
else:
country = self.current_mission.country(self.game.enemy_country)
if cp.id in self.game.planners.keys():
self.airgen.generate_flights(cp, country, self.game.planners[cp.id])
self.airgen.generate_flights(
cp,
country,
self.game.planners[cp.id],
self.groundobjectgen.runways
)
# Generate ground units on frontline everywhere
self.game.jtacs = []
jtacs: List[JtacInfo] = []
for player_cp, enemy_cp in self.game.theater.conflicts(True):
conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name,
self.current_mission.country(self.attacker_country),
@ -165,6 +211,7 @@ class Operation:
enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id])
groundConflictGen.generate()
jtacs.extend(groundConflictGen.jtacs)
# Setup combined arms parameters
self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0
@ -205,8 +252,8 @@ class Operation:
if not self.game.settings.jtac_smoke_on:
smoke = "false"
for jtac in self.game.jtacs:
script = script + "\n" + "JTACAutoLase('" + str(jtac[2]) + "', " + str(jtac[1]) + ", " + smoke + ", \"vehicle\")" + "\n"
for jtac in jtacs:
script += f"\nJTACAutoLase('{jtac.unit_name}', {jtac.code}, {smoke}, 'vehicle')\n"
load_autolase.add_action(DoScript(String(script)))
self.current_mission.triggerrules.triggers.append(load_autolase)
@ -221,17 +268,49 @@ class Operation:
load_dcs_libe.add_action(DoScript(String(script)))
self.current_mission.triggerrules.triggers.append(load_dcs_libe)
# Briefing Generation
for i, tanker_type in enumerate(self.airsupportgen.generated_tankers):
self.briefinggen.append_frequency("Tanker {} ({})".format(TANKER_CALLSIGNS[i], tanker_type), "{}X/{} MHz AM".format(60+i, 130+i))
self.assign_channels_to_flights()
kneeboard_generator = KneeboardGenerator(self.current_mission)
for dynamic_runway in self.groundobjectgen.runways.values():
self.briefinggen.add_dynamic_runway(dynamic_runway)
for tanker in self.airsupportgen.air_support.tankers:
self.briefinggen.add_tanker(tanker)
kneeboard_generator.add_tanker(tanker)
if self.is_awacs_enabled:
self.briefinggen.append_frequency("AWACS", "233 MHz AM")
for awacs in self.airsupportgen.air_support.awacs:
self.briefinggen.add_awacs(awacs)
kneeboard_generator.add_awacs(awacs)
self.briefinggen.append_frequency("Flight", "251 MHz AM")
for jtac in jtacs:
self.briefinggen.add_jtac(jtac)
kneeboard_generator.add_jtac(jtac)
for flight in self.airgen.flights:
self.briefinggen.add_flight(flight)
kneeboard_generator.add_flight(flight)
# Generate the briefing
self.briefinggen.generate()
kneeboard_generator.generate()
def assign_channels_to_flights(self) -> None:
"""Assigns preset radio channels for client flights."""
for flight in self.airgen.flights:
if not flight.client_units:
continue
self.assign_channels_to_flight(flight)
def assign_channels_to_flight(self, flight: FlightData) -> None:
"""Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
except KeyError:
logging.warning(f"No aircraft data for {airframe.id}")
return
aircraft_data.channel_allocator.assign_channels_for_flight(
flight, self.airsupportgen.air_support)

View File

@ -9,6 +9,7 @@ from .environmentgen import *
from .groundobjectsgen import *
from .briefinggen import *
from .forcedoptionsgen import *
from .kneeboard import *
from . import naming

View File

@ -1,14 +1,28 @@
from dcs.action import ActivateGroup, AITaskPush, MessageToCoalition, MessageToAll
from dataclasses import dataclass
from typing import Type
from dcs import helicopters
from dcs.action import ActivateGroup, AITaskPush, MessageToAll
from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone
from dcs.helicopters import UH_1H
from dcs.terrain.terrain import NoParkingSlotError
from dcs.flyingunit import FlyingUnit
from dcs.helicopters import helicopter_map, UH_1H
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import TriggerOnce, Event
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings
from game.utils import nm_to_meter
from gen.airfields import RunwayData
from gen.airsupportgen import AirSupport
from gen.callsigns import create_group_callsign_from_unit
from gen.flights.ai_flight_planner import FlightPlanner
from gen.flights.flight import Flight, FlightType, FlightWaypointType
from gen.flights.flight import (
Flight,
FlightType,
FlightWaypoint,
FlightWaypointType,
)
from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry
from .conflictgen import *
from .naming import *
@ -23,22 +37,471 @@ RTB_ALTITUDE = 800
RTB_DISTANCE = 5000
HELI_ALT = 500
# Note that fallback radio channels will *not* be reserved. It's possible that
# flights using these will overlap with other channels. This is because we would
# need to make sure we fell back to a frequency that is not used by any beacon
# or ATC, which we don't have the information to predict. Deal with the minor
# annoyance for now since we'll be fleshing out radio info soon enough.
ALLIES_WW2_CHANNEL = MHz(124)
GERMAN_WW2_CHANNEL = MHz(40)
HELICOPTER_CHANNEL = MHz(127)
UHF_FALLBACK_CHANNEL = MHz(251)
# TODO: Get radio information for all the special cases.
def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
if unit_type in helicopter_map.values() and unit_type != UH_1H:
return HELICOPTER_CHANNEL
german_ww2_aircraft = [
Bf_109K_4,
FW_190A8,
FW_190D9,
Ju_88A4,
]
if unit_type in german_ww2_aircraft:
return GERMAN_WW2_CHANNEL
allied_ww2_aircraft = [
I_16,
P_47D_30,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
]
if unit_type in allied_ww2_aircraft:
return ALLIES_WW2_CHANNEL
return UHF_FALLBACK_CHANNEL
class ChannelNamer:
"""Base class allowing channel name customization per-aircraft.
Most aircraft will want to customize this behavior, but the default is
reasonable for any aircraft with numbered radios.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
"""Returns the name of the channel for the given radio and channel."""
return f"COMM{radio_id} Ch {channel_id}"
class MirageChannelNamer(ChannelNamer):
"""Channel namer for the M-2000."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["V/UHF", "UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
class TomcatChannelNamer(ChannelNamer):
"""Channel namer for the F-14."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
class ViggenChannelNamer(ChannelNamer):
"""Channel namer for the AJS37."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id >= 4:
channel_letter = "EFGH"[channel_id - 4]
return f"FR 24 {channel_letter}"
return f"FR 22 Special {channel_id}"
class ViperChannelNamer(ChannelNamer):
"""Channel namer for the F-16."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM{radio_id} Ch {channel_id}"
class SCR522ChannelNamer(ChannelNamer):
"""
Channel namer for P-51 & P-47D
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id > 3:
return "?"
else:
return f"Button " + "ABCD"[channel_id - 1]
@dataclass(frozen=True)
class ChannelAssignment:
radio_id: int
channel: int
@dataclass
class FlightData:
"""Details of a planned flight."""
flight_type: FlightType
#: All units in the flight.
units: List[FlyingUnit]
#: Total number of aircraft in the flight.
size: int
#: True if this flight belongs to the player's coalition.
friendly: bool
#: Number of minutes after mission start the flight is set to depart.
departure_delay: int
#: Arrival airport.
arrival: RunwayData
#: Departure airport.
departure: RunwayData
#: Diver airport.
divert: Optional[RunwayData]
#: Waypoints of the flight plan.
waypoints: List[FlightWaypoint]
#: Radio frequency for intra-flight communications.
intra_flight_channel: RadioFrequency
#: Map of radio frequencies to their assigned radio and channel, if any.
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment]
def __init__(self, flight_type: FlightType, units: List[FlyingUnit],
size: int, friendly: bool, departure_delay: int,
departure: RunwayData, arrival: RunwayData,
divert: Optional[RunwayData], waypoints: List[FlightWaypoint],
intra_flight_channel: RadioFrequency) -> None:
self.flight_type = flight_type
self.units = units
self.size = size
self.friendly = friendly
self.departure_delay = departure_delay
self.departure = departure
self.arrival = arrival
self.divert = divert
self.waypoints = waypoints
self.intra_flight_channel = intra_flight_channel
self.frequency_to_channel_map = {}
self.callsign = create_group_callsign_from_unit(self.units[0])
@property
def client_units(self) -> List[FlyingUnit]:
"""List of playable units in the flight."""
return [u for u in self.units if u.is_human()]
@property
def aircraft_type(self) -> FlyingType:
"""Returns the type of aircraft in this flight."""
return self.units[0].unit_type
def num_radio_channels(self, radio_id: int) -> int:
"""Returns the number of preset channels for the given radio."""
# Note: pydcs only initializes the radio presets for client slots.
return self.client_units[0].num_radio_channels(radio_id)
def channel_for(
self, frequency: RadioFrequency) -> Optional[ChannelAssignment]:
"""Returns the radio and channel number for the given frequency."""
return self.frequency_to_channel_map.get(frequency, None)
def assign_channel(self, radio_id: int, channel_id: int,
frequency: RadioFrequency) -> None:
"""Assigns a preset radio channel to the given frequency."""
for unit in self.client_units:
unit.set_radio_channel_preset(radio_id, channel_id, frequency.mhz)
# One frequency could be bound to multiple channels. Prefer the first,
# since with the current implementation it will be the lowest numbered
# channel.
if frequency not in self.frequency_to_channel_map:
self.frequency_to_channel_map[frequency] = ChannelAssignment(
radio_id, channel_id
)
class RadioChannelAllocator:
"""Base class for radio channel allocators."""
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
"""Assigns mission frequencies to preset channels for the flight."""
raise NotImplementedError
@dataclass(frozen=True)
class CommonRadioChannelAllocator(RadioChannelAllocator):
"""Radio channel allocator suitable for most aircraft.
Most of the aircraft with preset channels available have one or more radios
with 20 or more channels available (typically per-radio, but this is not the
case for the JF-17).
"""
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
inter_flight_radio_index: Optional[int]
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
intra_flight_radio_index: Optional[int]
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
flight.assign_channel(
self.intra_flight_radio_index, 1, flight.intra_flight_channel)
# For cases where the inter-flight and intra-flight radios share presets
# (the JF-17 only has one set of channels, even though it can use two
# channels simultaneously), start assigning inter-flight channels at 2.
radio_id = self.inter_flight_radio_index
if self.intra_flight_radio_index == radio_id:
first_channel = 2
else:
first_channel = 1
last_channel = flight.num_radio_channels(radio_id)
channel_alloc = iter(range(first_channel, last_channel + 1))
flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
# TODO: If there ever are multiple AWACS, limit to mission relevant.
for awacs in air_support.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
if flight.arrival != flight.departure:
flight.assign_channel(radio_id, next(channel_alloc),
flight.arrival.atc)
try:
# TODO: Skip incompatible tankers.
for tanker in air_support.tankers:
flight.assign_channel(
radio_id, next(channel_alloc), tanker.freq)
if flight.divert is not None:
flight.assign_channel(radio_id, next(channel_alloc),
flight.divert.atc)
except StopIteration:
# Any remaining channels are nice-to-haves, but not necessary for
# the few aircraft with a small number of channels available.
pass
@dataclass(frozen=True)
class WarthogRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the A-10C."""
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
# The A-10's radio works differently than most aircraft. Doesn't seem to
# be a way to set these from the mission editor, let alone pydcs.
pass
@dataclass(frozen=True)
class ViggenRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the AJS37."""
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
# The Viggen's preset channels are handled differently from other
# aircraft. The aircraft automatically configures channels for every
# allied flight in the game (including AWACS) and for every airfield. As
# such, we don't need to allocate any of those. There are seven presets
# we can modify, however: three channels for the main radio intended for
# communication with wingmen, and four emergency channels for the backup
# radio. We'll set the first channel of the main radio to the
# intra-flight channel, and the first three emergency channels to each
# of the flight plan's airfields. The fourth emergency channel is always
# the guard channel.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
flight.assign_channel(radio_id, 4, flight.departure.atc)
flight.assign_channel(radio_id, 5, flight.arrival.atc)
# TODO: Assign divert to 6 when we support divert airfields.
@dataclass(frozen=True)
class SCR522RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
flight.assign_channel(radio_id, 2, flight.departure.atc)
flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
@dataclass(frozen=True)
class AircraftData:
"""Additional aircraft data not exposed by pydcs."""
#: The type of radio used for inter-flight communications.
inter_flight_radio: Radio
#: The type of radio used for intra-flight communications.
intra_flight_radio: Radio
#: The radio preset channel allocator, if the aircraft supports channel
#: presets. If the aircraft does not support preset channels, this will be
#: None.
channel_allocator: Optional[RadioChannelAllocator]
#: Defines how channels should be named when printed in the kneeboard.
channel_namer: Type[ChannelNamer] = ChannelNamer
# Indexed by the id field of the pydcs PlaneType.
AIRCRAFT_DATA: Dict[str, AircraftData] = {
"A-10C": AircraftData(
inter_flight_radio=get_radio("AN/ARC-164"),
intra_flight_radio=get_radio("AN/ARC-186(V) AM"),
channel_allocator=WarthogRadioChannelAllocator()
),
"AJS37": AircraftData(
# The AJS37 has somewhat unique radio configuration. Two backup radio
# (FR 24) can only operate simultaneously with the main radio in guard
# mode. As such, we only use the main radio for both inter- and intra-
# flight communication.
inter_flight_radio=get_radio("FR 22"),
intra_flight_radio=get_radio("FR 22"),
channel_allocator=ViggenRadioChannelAllocator(),
channel_namer=ViggenChannelNamer
),
"AV8BNA": AircraftData(
inter_flight_radio=get_radio("AN/ARC-210"),
intra_flight_radio=get_radio("AN/ARC-210"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=2,
intra_flight_radio_index=1
)
),
"F-14B": AircraftData(
inter_flight_radio=get_radio("AN/ARC-159"),
intra_flight_radio=get_radio("AN/ARC-182"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=2
),
channel_namer=TomcatChannelNamer
),
"F-16C_50": AircraftData(
inter_flight_radio=get_radio("AN/ARC-164"),
intra_flight_radio=get_radio("AN/ARC-222"),
# COM2 is the AN/ARC-222, which is the VHF radio we want to use for
# intra-flight communication to leave COM1 open for UHF inter-flight.
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=2
),
channel_namer=ViperChannelNamer
),
"FA-18C_hornet": AircraftData(
inter_flight_radio=get_radio("AN/ARC-210"),
intra_flight_radio=get_radio("AN/ARC-210"),
# DCS will clobber channel 1 of the first radio compatible with the
# flight's assigned frequency. Since the F/A-18's two radios are both
# AN/ARC-210s, radio 1 will be compatible regardless of which frequency
# is assigned, so we must use radio 1 for the intra-flight radio.
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=2,
intra_flight_radio_index=1
)
),
"JF-17": AircraftData(
inter_flight_radio=get_radio("R&S M3AR UHF"),
intra_flight_radio=get_radio("R&S M3AR VHF"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=1
),
# Same naming pattern as the Viper, so just reuse that.
channel_namer=ViperChannelNamer
),
"M-2000C": AircraftData(
inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"),
intra_flight_radio=get_radio("TRT ERA 7200 UHF"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=2
),
channel_namer=MirageChannelNamer
),
"P-51D": AircraftData(
inter_flight_radio=get_radio("SCR522"),
intra_flight_radio=get_radio("SCR522"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=1
),
channel_namer=SCR522ChannelNamer
),
}
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
class AircraftConflictGenerator:
escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]]
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, game):
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
game, radio_registry: RadioRegistry):
self.m = mission
self.game = game
self.settings = settings
self.conflict = conflict
self.radio_registry = radio_registry
self.escort_targets = []
self.flights: List[FlightData] = []
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
"""Allocates an intra-flight channel to a group.
Args:
airframe: The type of aircraft a channel should be allocated for.
Returns:
The frequency of the intra-flight channel.
"""
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
return self.radio_registry.alloc_for_radio(
aircraft_data.intra_flight_radio)
except KeyError:
return get_fallback_channel(airframe)
def _start_type(self) -> StartType:
return self.settings.cold_start and StartType.Cold or StartType.Warm
def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], flight: Flight):
def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task],
flight: Flight, dynamic_runways: Dict[str, RunwayData]):
did_load_loadout = False
unit_type = group.units[0].unit_type
@ -74,10 +537,11 @@ class AircraftConflictGenerator:
single_client = flight.client_count == 1
for idx in range(0, min(len(group.units), flight.client_count)):
unit = group.units[idx]
if single_client:
group.units[idx].set_player()
unit.set_player()
else:
group.units[idx].set_client()
unit.set_client()
# Do not generate player group with late activation.
if group.late_activation:
@ -85,24 +549,42 @@ class AircraftConflictGenerator:
# Set up F-14 Client to have pre-stored alignement
if unit_type is F_14B:
group.units[idx].set_property(F_14B.Properties.INSAlignmentStored.id, True)
unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
# TODO : refactor this following bad specific special case code :(
channel = self.get_intra_flight_channel(unit_type)
group.set_frequency(channel.mhz)
if unit_type in helicopters.helicopter_map.values() and unit_type not in [UH_1H]:
group.set_frequency(127.5)
# TODO: Support for different departure/arrival airfields.
cp = flight.from_cp
fallback_runway = RunwayData(cp.full_name, runway_name="")
if cp.cptype == ControlPointType.AIRBASE:
departure_runway = self.get_preferred_runway(flight.from_cp.airport)
elif cp.is_fleet:
departure_runway = dynamic_runways.get(cp.name, fallback_runway)
else:
if unit_type not in [P_51D_30_NA, P_51D, SpitfireLFMkIX, SpitfireLFMkIXCW, P_47D_30, I_16, FW_190A8, FW_190D9, Bf_109K_4]:
group.set_frequency(251.0)
else:
# WW2
if unit_type in [FW_190A8, FW_190D9, Bf_109K_4, Ju_88A4]:
group.set_frequency(40)
else:
group.set_frequency(124.0)
logging.warning(f"Unhandled departure control point: {cp.cptype}")
departure_runway = fallback_runway
# The first waypoint is set automatically by pydcs, so it's not in our
# list. Convert the pydcs MovingPoint to a FlightWaypoint so it shows up
# in our FlightData.
first_point = FlightWaypoint.from_pydcs(group.points[0], flight.from_cp)
self.flights.append(FlightData(
flight_type=flight.flight_type,
units=group.units,
size=len(group.units),
friendly=flight.from_cp.captured,
departure_delay=flight.scheduled_in,
departure=departure_runway,
arrival=departure_runway,
# TODO: Support for divert airfields.
divert=None,
waypoints=[first_point] + flight.points,
intra_flight_channel=channel
))
# Special case so Su 33 carrier take off
if unit_type is Su_33:
@ -113,6 +595,21 @@ class AircraftConflictGenerator:
for unit in group.units:
unit.fuel = Su_33.fuel_max * 0.8
def get_preferred_runway(self, airport: Airport) -> RunwayData:
"""Returns the preferred runway for the given airport.
Right now we're only selecting runways based on whether or not they have
ILS, but we could also choose based on wind conditions, or which
direction flight plans should follow.
"""
runways = list(RunwayData.for_pydcs_airport(airport))
for runway in runways:
# Prefer any runway with ILS.
if runway.ils is not None:
return runway
# Otherwise we lack the mission information to pick more usefully,
# so just use the first runway.
return runways[0]
def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, airport: Airport = None, start_type = None) -> FlyingGroup:
assert count > 0
@ -253,17 +750,18 @@ class AircraftConflictGenerator:
logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
def generate_flights(self, cp, country, flight_planner:FlightPlanner):
def generate_flights(self, cp, country, flight_planner: FlightPlanner,
dynamic_runways: Dict[str, RunwayData]):
# Clear pydcs parking slots
logging.info("CLEARING SLOTS @ " + cp.airport.name)
logging.info("===============")
if cp.airport is not None:
for ps in cp.airport.parking_slots:
logging.info("SLOT : " + str(ps.unit_id))
ps.unit_id = None
logging.info("----------------")
logging.info("===============")
logging.info("CLEARING SLOTS @ " + cp.airport.name)
logging.info("===============")
if cp.airport is not None:
for ps in cp.airport.parking_slots:
logging.info("SLOT : " + str(ps.unit_id))
ps.unit_id = None
logging.info("----------------")
logging.info("===============")
for flight in flight_planner.flights:
@ -272,7 +770,8 @@ class AircraftConflictGenerator:
continue
logging.info("Generating flight : " + str(flight.unit_type))
group = self.generate_planned_flight(cp, country, flight)
self.setup_flight_group(group, flight, flight.flight_type)
self.setup_flight_group(group, flight, flight.flight_type,
dynamic_runways)
self.setup_group_activation_trigger(flight, group)
@ -383,19 +882,13 @@ class AircraftConflictGenerator:
flight.group = group
return group
def setup_group_as_intercept_flight(self, group, flight):
group.points[0].ETA = 0
group.late_activation = True
self._setup_group(group, Intercept, flight)
for point in flight.points:
group.add_waypoint(Point(point.x,point.y), point.alt)
def setup_flight_group(self, group, flight, flight_type):
def setup_flight_group(self, group, flight, flight_type,
dynamic_runways: Dict[str, RunwayData]):
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION]:
group.task = CAP.name
self._setup_group(group, CAP, flight)
self._setup_group(group, CAP, flight, dynamic_runways)
# group.points[0].tasks.clear()
group.points[0].tasks.clear()
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), targets=[Targets.All.Air]))
@ -407,7 +900,7 @@ class AircraftConflictGenerator:
elif flight_type in [FlightType.CAS, FlightType.BAI]:
group.task = CAS.name
self._setup_group(group, CAS, flight)
self._setup_group(group, CAS, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(10), targets=[Targets.All.GroundUnits.GroundVehicles]))
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
@ -416,7 +909,7 @@ class AircraftConflictGenerator:
group.points[0].tasks.append(OptRestrictJettison(True))
elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
group.task = SEAD.name
self._setup_group(group, SEAD, flight)
self._setup_group(group, SEAD, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(NoTask())
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
@ -425,14 +918,14 @@ class AircraftConflictGenerator:
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.ASM))
elif flight_type in [FlightType.STRIKE]:
group.task = PinpointStrike.name
self._setup_group(group, GroundAttack, flight)
self._setup_group(group, GroundAttack, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
group.points[0].tasks.append(OptRestrictJettison(True))
elif flight_type in [FlightType.ANTISHIP]:
group.task = AntishipStrike.name
self._setup_group(group, AntishipStrike, flight)
self._setup_group(group, AntishipStrike, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
@ -511,23 +1004,3 @@ class AircraftConflictGenerator:
pt.name = String(point.name)
self._setup_custom_payload(flight, group)
def setup_group_as_antiship_flight(self, group, flight):
group.task = AntishipStrike.name
self._setup_group(group, AntishipStrike, flight)
group.points[0].tasks.clear()
group.points[0].tasks.append(AntishipStrikeTaskAction())
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFireWeaponFree))
group.points[0].tasks.append(OptRestrictJettison(True))
for point in flight.points:
group.add_waypoint(Point(point.x, point.y), point.alt)
def setup_radio_preset(self, flight, group):
pass

1558
gen/airfields.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,10 @@
from game import db
from dataclasses import dataclass, field
from .callsigns import callsign_for_support_unit
from .conflictgen import *
from .naming import *
from dcs.mission import *
from dcs.unitgroup import *
from dcs.unittype import *
from dcs.task import *
from dcs.terrain.terrain import NoParkingSlotError
from .radios import RadioFrequency, RadioRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry
TANKER_DISTANCE = 15000
TANKER_ALT = 4572
@ -16,14 +14,39 @@ AWACS_DISTANCE = 150000
AWACS_ALT = 13000
class AirSupportConflictGenerator:
generated_tankers = None # type: typing.List[str]
@dataclass
class AwacsInfo:
"""AWACS information for the kneeboard."""
callsign: str
freq: RadioFrequency
def __init__(self, mission: Mission, conflict: Conflict, game):
@dataclass
class TankerInfo:
"""Tanker information for the kneeboard."""
callsign: str
variant: str
freq: RadioFrequency
tacan: TacanChannel
@dataclass
class AirSupport:
awacs: List[AwacsInfo] = field(default_factory=list)
tankers: List[TankerInfo] = field(default_factory=list)
class AirSupportConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry) -> None:
self.mission = mission
self.conflict = conflict
self.game = game
self.generated_tankers = []
self.air_support = AirSupport()
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
@classmethod
def support_tasks(cls) -> typing.Collection[typing.Type[MainTask]]:
@ -32,9 +55,12 @@ class AirSupportConflictGenerator:
def generate(self, is_awacs_enabled):
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp
CALLSIGNS = ["TKR", "TEX", "FUL", "FUE", ""]
fallback_tanker_number = 0
for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)):
self.generated_tankers.append(db.unit_type_name(tanker_unit_type))
variant = db.unit_type_name(tanker_unit_type)
freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i
tanker_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE)
tanker_group = self.mission.refuel_flight(
@ -45,21 +71,42 @@ class AirSupportConflictGenerator:
position=tanker_position,
altitude=TANKER_ALT,
race_distance=58000,
frequency=130 + i,
frequency=freq.mhz,
start_type=StartType.Warm,
speed=574,
tacanchannel="{}X".format(60 + i),
tacanchannel=str(tacan),
)
callsign = callsign_for_support_unit(tanker_group)
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(callsign)
if tacan_callsign is None:
# The dict above is all the callsigns currently in the game, but
# non-Western countries don't use the callsigns and instead just
# use numbers. It's possible that none of those nations have
# TACAN compatible refueling aircraft, but fallback just in
# case.
tacan_callsign = f"TK{fallback_tanker_number}"
fallback_tanker_number += 1
if tanker_unit_type != IL_78M:
tanker_group.points[0].tasks.pop() # Override PyDCS tacan channel
tanker_group.points[0].tasks.append(ActivateBeaconCommand(60 + i, "X", CALLSIGNS[i], True, tanker_group.units[0].id, True))
# Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append(ActivateBeaconCommand(
tacan.number, tacan.band.value, tacan_callsign, True,
tanker_group.units[0].id, True))
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(TankerInfo(callsign, variant, freq, tacan))
if is_awacs_enabled:
try:
freq = self.radio_registry.alloc_uhf()
awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0]
awacs_flight = self.mission.awacs_flight(
country=self.mission.country(self.game.player_country),
@ -68,11 +115,13 @@ class AirSupportConflictGenerator:
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
frequency=233,
frequency=freq.mhz,
start_type=StartType.Warm,
)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
except:
print("No AWACS for faction")
self.air_support.awacs.append(AwacsInfo(
callsign_for_support_unit(awacs_flight), freq))
except:
print("No AWACS for faction")

View File

@ -1,10 +1,12 @@
from dcs.action import AITaskPush, AITaskSet
from dataclasses import dataclass
from dcs.action import AITaskPush
from dcs.condition import TimeAfter, UnitDamaged, Or, GroupLifeLess
from dcs.task import *
from dcs.triggers import TriggerOnce, Event
from gen import namegen
from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE
from .callsigns import callsign_for_support_unit
from .conflictgen import *
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
@ -22,6 +24,17 @@ FIGHT_DISTANCE = 3500
RANDOM_OFFSET_ATTACK = 250
@dataclass(frozen=True)
class JtacInfo:
"""JTAC information."""
unit_name: str
callsign: str
region: str
code: str
# TODO: Radio info? Type?
class GroundConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance):
@ -32,6 +45,7 @@ class GroundConflictGenerator:
self.player_stance = CombatStance(player_stance)
self.enemy_stance = random.choice([CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESSIVE])
self.game = game
self.jtacs: List[JtacInfo] = []
def _group_point(self, point) -> Point:
distance = randint(
@ -100,7 +114,7 @@ class GroundConflictGenerator:
# Add JTAC
if "has_jtac" in self.game.player_faction and self.game.player_faction["has_jtac"] and self.game.settings.include_jtac_if_available:
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
code = 1688 - len(self.game.jtacs)
code = 1688 - len(self.jtacs)
utype = MQ_9_Reaper
if "jtac_unit" in self.game.player_faction:
@ -115,7 +129,10 @@ class GroundConflictGenerator:
jtac.points[0].tasks.append(SetInvisibleCommand(True))
jtac.points[0].tasks.append(SetImmortalCommand(True))
jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle))
self.game.jtacs.append(("Frontline " + self.conflict.from_cp.name + "/" + self.conflict.to_cp.name, code, n))
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
# Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac)
self.jtacs.append(JtacInfo(n, callsign, frontline, str(code)))
def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading):

74
gen/beacons.py Normal file
View File

@ -0,0 +1,74 @@
from dataclasses import dataclass
from enum import auto, IntEnum
import json
from pathlib import Path
from typing import Iterable, Optional
from gen.radios import RadioFrequency
from gen.tacan import TacanBand, TacanChannel
BEACONS_RESOURCE_PATH = Path("resources/dcs/beacons")
class BeaconType(IntEnum):
BEACON_TYPE_NULL = auto()
BEACON_TYPE_VOR = auto()
BEACON_TYPE_DME = auto()
BEACON_TYPE_VOR_DME = auto()
BEACON_TYPE_TACAN = auto()
BEACON_TYPE_VORTAC = auto()
BEACON_TYPE_RSBN = auto()
BEACON_TYPE_BROADCAST_STATION = auto()
BEACON_TYPE_HOMER = auto()
BEACON_TYPE_AIRPORT_HOMER = auto()
BEACON_TYPE_AIRPORT_HOMER_WITH_MARKER = auto()
BEACON_TYPE_ILS_FAR_HOMER = auto()
BEACON_TYPE_ILS_NEAR_HOMER = auto()
BEACON_TYPE_ILS_LOCALIZER = auto()
BEACON_TYPE_ILS_GLIDESLOPE = auto()
BEACON_TYPE_PRMG_LOCALIZER = auto()
BEACON_TYPE_PRMG_GLIDESLOPE = auto()
BEACON_TYPE_ICLS_LOCALIZER = auto()
BEACON_TYPE_ICLS_GLIDESLOPE = auto()
BEACON_TYPE_NAUTICAL_HOMER = auto()
@dataclass(frozen=True)
class Beacon:
name: str
callsign: str
beacon_type: BeaconType
hertz: int
channel: Optional[int]
@property
def frequency(self) -> RadioFrequency:
return RadioFrequency(self.hertz)
@property
def is_tacan(self) -> bool:
return self.beacon_type in (
BeaconType.BEACON_TYPE_VORTAC,
BeaconType.BEACON_TYPE_TACAN,
)
@property
def tacan_channel(self) -> TacanChannel:
assert self.is_tacan
assert self.channel is not None
return TacanChannel(self.channel, TacanBand.X)
def load_beacons_for_terrain(name: str) -> Iterable[Beacon]:
beacons_file = BEACONS_RESOURCE_PATH / f"{name.lower()}.json"
if not beacons_file.exists():
raise RuntimeError(f"Beacon file {beacons_file.resolve()} is missing")
for beacon in json.loads(beacons_file.read_text()):
yield Beacon(**beacon)

View File

@ -1,68 +1,127 @@
import logging
import os
from collections import defaultdict
from dataclasses import dataclass
import random
from typing import List
from game import db
from .conflictgen import *
from .naming import *
from dcs.mission import *
from dcs.mission import Mission
from .aircraft import FlightData
from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo
from .armor import JtacInfo
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency
class BriefingGenerator:
freqs = None # type: typing.List[typing.Tuple[str, str]]
title = "" # type: str
description = "" # type: str
targets = None # type: typing.List[typing.Tuple[str, str]]
waypoints = None # type: typing.List[str]
@dataclass
class CommInfo:
"""Communications information for the kneeboard."""
name: str
freq: RadioFrequency
class MissionInfoGenerator:
"""Base type for generators of mission information for the player.
Examples of subtypes include briefing generators, kneeboard generators, etc.
"""
def __init__(self, mission: Mission) -> None:
self.mission = mission
self.awacs: List[AwacsInfo] = []
self.comms: List[CommInfo] = []
self.flights: List[FlightData] = []
self.jtacs: List[JtacInfo] = []
self.tankers: List[TankerInfo] = []
def add_awacs(self, awacs: AwacsInfo) -> None:
"""Adds an AWACS/GCI to the mission.
Args:
awacs: AWACS information.
"""
self.awacs.append(awacs)
def add_comm(self, name: str, freq: RadioFrequency) -> None:
"""Adds communications info to the mission.
Args:
name: Name of the radio channel.
freq: Frequency of the radio channel.
"""
self.comms.append(CommInfo(name, freq))
def add_flight(self, flight: FlightData) -> None:
"""Adds flight info to the mission.
Args:
flight: Flight information.
"""
self.flights.append(flight)
def add_jtac(self, jtac: JtacInfo) -> None:
"""Adds a JTAC to the mission.
Args:
jtac: JTAC information.
"""
self.jtacs.append(jtac)
def add_tanker(self, tanker: TankerInfo) -> None:
"""Adds a tanker to the mission.
Args:
tanker: Tanker information.
"""
self.tankers.append(tanker)
def generate(self) -> None:
"""Generates the mission information."""
raise NotImplementedError
class BriefingGenerator(MissionInfoGenerator):
def __init__(self, mission: Mission, conflict: Conflict, game):
self.m = mission
super().__init__(mission)
self.conflict = conflict
self.game = game
self.title = ""
self.description = ""
self.dynamic_runways: List[RunwayData] = []
self.freqs = []
self.targets = []
self.waypoints = []
def add_dynamic_runway(self, runway: RunwayData) -> None:
"""Adds a dynamically generated runway to the briefing.
self.jtacs = []
Dynamic runways are any valid landing point that is a unit rather than a
map feature. These include carriers, ships with a helipad, and FARPs.
"""
self.dynamic_runways.append(runway)
def append_frequency(self, name: str, frequency: str):
self.freqs.append((name, frequency))
def add_flight_description(self, flight: FlightData):
assert flight.client_units
def append_target(self, description: str, markpoint: str = None):
self.targets.append((description, markpoint))
def append_waypoint(self, description: str):
self.waypoints.append(description)
def add_flight_description(self, flight):
if flight.client_count <= 0:
return
flight_unit_name = db.unit_type_name(flight.unit_type)
aircraft = flight.aircraft_type
flight_unit_name = db.unit_type_name(aircraft)
self.description += "-" * 50 + "\n"
self.description += flight_unit_name + " x " + str(flight.count) + 2 * "\n"
self.description += f"{flight_unit_name} x {flight.size + 2}\n\n"
self.description += "#0 -- TAKEOFF : Take off from " + flight.from_cp.name + "\n"
for i, wpt in enumerate(flight.points):
self.description += "#" + str(1+i) + " -- " + wpt.name + " : " + wpt.description + "\n"
self.description += "#" + str(len(flight.points) + 1) + " -- RTB\n\n"
for i, wpt in enumerate(flight.waypoints):
self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n"
self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n"
group = flight.group
if group is not None:
for i, nav_target in enumerate(group.nav_target_points):
self.description += nav_target.text_comment + "\n"
self.description += "\n"
self.description += "-" * 50 + "\n"
def add_ally_flight_description(self, flight):
if flight.client_count == 0:
flight_unit_name = db.unit_type_name(flight.unit_type)
self.description += flight.flight_type.name + " " + flight_unit_name + " x " + str(flight.count) + ", departing in " + str(flight.scheduled_in) + " minutes \n"
def add_ally_flight_description(self, flight: FlightData):
assert not flight.client_units
aircraft = flight.aircraft_type
flight_unit_name = db.unit_type_name(aircraft)
self.description += (
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
f"departing in {flight.departure_delay} minutes\n"
)
def generate(self):
self.description = ""
self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n"
@ -74,52 +133,50 @@ class BriefingGenerator:
self.description += "Your flights:" + "\n"
self.description += "=" * 15 + "\n\n"
for planner in self.game.planners.values():
for flight in planner.flights:
for flight in self.flights:
if flight.client_units:
self.add_flight_description(flight)
self.description += "\n"*2
self.description += "Planned ally flights:" + "\n"
self.description += "=" * 15 + "\n"
for planner in self.game.planners.values():
if planner.from_cp.captured and len(planner.flights) > 0:
self.description += "\nFrom " + planner.from_cp.full_name + " \n"
self.description += "-" * 50 + "\n\n"
for flight in planner.flights:
self.add_ally_flight_description(flight)
allied_flights_by_departure = defaultdict(list)
for flight in self.flights:
if not flight.client_units and flight.friendly:
name = flight.departure.airfield_name
allied_flights_by_departure[name].append(flight)
for departure, flights in allied_flights_by_departure.items():
self.description += f"\nFrom {departure}\n"
self.description += "-" * 50 + "\n\n"
for flight in flights:
self.add_ally_flight_description(flight)
if self.freqs:
if self.comms:
self.description += "\n\nComms Frequencies:\n"
self.description += "=" * 15 + "\n"
for name, freq in self.freqs:
self.description += "{}: {}\n".format(name, freq)
for comm_info in self.comms:
self.description += f"{comm_info.name}: {comm_info.freq}\n"
self.description += ("-" * 50) + "\n"
for cp in self.game.theater.controlpoints:
if cp.captured and cp.cptype in [ControlPointType.LHA_GROUP, ControlPointType.AIRCRAFT_CARRIER_GROUP]:
self.description += cp.name + "\n"
self.description += "RADIO : 127.5 Mhz AM\n"
self.description += "TACAN : "
self.description += str(cp.tacanN)
if cp.tacanY:
self.description += "Y"
else:
self.description += "X"
self.description += " " + str(cp.tacanI) + "\n"
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP and hasattr(cp, "icls"):
self.description += "ICLS Channel : " + str(cp.icls) + "\n"
self.description += "-" * 50 + "\n"
for runway in self.dynamic_runways:
self.description += f"{runway.airfield_name}\n"
self.description += f"RADIO : {runway.atc}\n"
if runway.tacan is not None:
self.description += f"TACAN : {runway.tacan} {runway.tacan_callsign}\n"
if runway.icls is not None:
self.description += f"ICLS Channel : {runway.icls}\n"
self.description += "-" * 50 + "\n"
self.description += "JTACS [F-10 Menu] : \n"
self.description += "===================\n\n"
for jtac in self.game.jtacs:
self.description += str(jtac[0]) + " -- Code : " + str(jtac[1]) + "\n"
for jtac in self.jtacs:
self.description += f"{jtac.region} -- Code : {jtac.code}\n"
self.m.set_description_text(self.description)
self.mission.set_description_text(self.description)
self.m.add_picture_blue(os.path.abspath("./resources/ui/splash_screen.png"))
self.mission.add_picture_blue(os.path.abspath(
"./resources/ui/splash_screen.png"))
def generate_ongoing_war_text(self):
@ -180,7 +237,7 @@ class BriefingGenerator:
def __random_frontline_sentence(self, player_base_name, enemy_base_name):
templates = [
"There are combats between {} and {}. ",
"The war on the ground is still going on between {} an {}. ",
"The war on the ground is still going on between {} and {}. ",
"Our ground forces in {} are opposed to enemy forces based in {}. ",
"Our forces from {} are fighting enemies based in {}. ",
"There is an active frontline between {} and {}. ",

34
gen/callsigns.py Normal file
View File

@ -0,0 +1,34 @@
"""Support for working with DCS group callsigns."""
import logging
import re
from dcs.unitgroup import FlyingGroup
from dcs.flyingunit import FlyingUnit
def callsign_for_support_unit(group: FlyingGroup) -> str:
# Either something like Overlord11 for Western AWACS, or else just a number.
# Convert to either "Overlord" or "Flight 123".
lead = group.units[0]
raw_callsign = lead.callsign_as_str()
try:
return f"Flight {int(raw_callsign)}"
except ValueError:
return raw_callsign.rstrip("1234567890")
def create_group_callsign_from_unit(lead: FlyingUnit) -> str:
raw_callsign = lead.callsign_as_str()
if not lead.callsign_is_western:
# Callsigns for non-Western countries are just a number per flight,
# similar to tail numbers.
return f"Flight {raw_callsign}"
# Callsign from pydcs is in the format `<name><group ID><unit ID>`,
# where unit ID is guaranteed to be a single digit but the group ID may
# be more.
match = re.search(r"^(\D+)(\d+)(\d)$", raw_callsign)
if match is None:
logging.error(f"Could not parse unit callsign: {raw_callsign}")
return f"Flight {raw_callsign}"
return f"{match.group(1)} {match.group(2)}"

View File

@ -1,7 +1,7 @@
import logging
import typing
import pdb
from pydcs import dcs
import dcs
from random import randint
from dcs import Mission

View File

@ -3,22 +3,38 @@ import random
from dcs.vehicles import Armor
from game import db
from gen.defenses.armored_group_generator import ArmoredGroupGenerator
from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator
def generate_armor_group(faction:str, game, ground_object):
"""
This generate a group of ground units
:param parentCp: The parent control point
:param ground_object: The ground object which will own the group
:param country: Owner country
:return: Generated group
"""
possible_unit = [u for u in db.FACTIONS[faction]["units"] if u in Armor.__dict__.values()]
if len(possible_unit) > 0:
unit_type = random.choice(possible_unit)
generator = ArmoredGroupGenerator(game, ground_object, unit_type)
generator.generate()
return generator.get_generated_group()
return generate_armor_group_of_type(game, ground_object, unit_type)
return None
def generate_armor_group_of_type(game, ground_object, unit_type):
"""
This generate a group of ground units of given type
:return: Generated group
"""
generator = ArmoredGroupGenerator(game, ground_object, unit_type)
generator.generate()
return generator.get_generated_group()
def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int):
"""
This generate a group of ground units of given type and size
:return: Generated group
"""
generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size)
generator.generate()
return generator.get_generated_group()

View File

@ -25,3 +25,20 @@ class ArmoredGroupGenerator(GroupGenerator):
self.position.y + spacing * j, self.heading)
class FixedSizeArmorGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type, size):
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
self.unit_type = unit_type
self.size = size
def generate(self):
spacing = random.randint(20, 70)
index = 0
for i in range(self.size):
index = index + 1
self.add_unit(self.unit_type, "Armor#" + str(index),
self.position.x + spacing * i,
self.position.y, self.heading)

View File

@ -372,17 +372,26 @@ class FlightPlanner:
egress_heading = heading - 180 - 25
ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
ingress_point = FlightWaypoint(ingress_pos.x, ingress_pos.y, self.doctrine["INGRESS_ALT"])
ingress_point = FlightWaypoint(
FlightWaypointType.INGRESS_STRIKE,
ingress_pos.x,
ingress_pos.y,
self.doctrine["INGRESS_ALT"]
)
ingress_point.pretty_name = "INGRESS on " + location.obj_name
ingress_point.description = "INGRESS on " + location.obj_name
ingress_point.name = "INGRESS"
ingress_point.waypoint_type = FlightWaypointType.INGRESS_STRIKE
flight.points.append(ingress_point)
if len(location.groups) > 0 and location.dcs_identifier == "AA":
for g in location.groups:
for j, u in enumerate(g.units):
point = FlightWaypoint(u.position.x, u.position.y, 0)
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
u.position.x,
u.position.y,
0
)
point.description = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j)
point.pretty_name = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j)
point.name = location.obj_name + "#" + str(j)
@ -398,7 +407,12 @@ class FlightPlanner:
if building.is_dead:
continue
point = FlightWaypoint(building.position.x, building.position.y, 0)
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
building.position.x,
building.position.y,
0
)
point.description = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]"
point.pretty_name = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]"
point.name = building.obj_name
@ -406,7 +420,12 @@ class FlightPlanner:
ingress_point.targets.append(building)
flight.points.append(point)
else:
point = FlightWaypoint(location.position.x, location.position.y, 0)
point = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
location.position.y,
0
)
point.description = "STRIKE on " + location.obj_name
point.pretty_name = "STRIKE on " + location.obj_name
point.name = location.obj_name
@ -415,11 +434,15 @@ class FlightPlanner:
flight.points.append(point)
egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
egress_point = FlightWaypoint(egress_pos.x, egress_pos.y, self.doctrine["EGRESS_ALT"])
egress_point = FlightWaypoint(
FlightWaypointType.EGRESS,
egress_pos.x,
egress_pos.y,
self.doctrine["EGRESS_ALT"]
)
egress_point.name = "EGRESS"
egress_point.pretty_name = "EGRESS from " + location.obj_name
egress_point.description = "EGRESS from " + location.obj_name
egress_point.waypoint_type = FlightWaypointType.EGRESS
flight.points.append(egress_point)
descend = self.generate_descend_point(flight.from_cp)
@ -454,18 +477,26 @@ class FlightPlanner:
ascend = self.generate_ascend_point(flight.from_cp)
flight.points.append(ascend)
orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt)
orbit0 = FlightWaypoint(
FlightWaypointType.PATROL_TRACK,
orbit0p.x,
orbit0p.y,
patrol_alt
)
orbit0.name = "ORBIT 0"
orbit0.description = "Standby between this point and the next one"
orbit0.pretty_name = "Race-track start"
orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK
flight.points.append(orbit0)
orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt)
orbit1 = FlightWaypoint(
FlightWaypointType.PATROL,
orbit1p.x,
orbit1p.y,
patrol_alt
)
orbit1.name = "ORBIT 1"
orbit1.description = "Standby between this point and the previous one"
orbit1.pretty_name = "Race-track end"
orbit1.waypoint_type = FlightWaypointType.PATROL
flight.points.append(orbit1)
orbit0.targets.append(for_cp)
@ -512,18 +543,26 @@ class FlightPlanner:
ascend = self.generate_ascend_point(flight.from_cp)
flight.points.append(ascend)
orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt)
orbit0 = FlightWaypoint(
FlightWaypointType.PATROL_TRACK,
orbit0p.x,
orbit0p.y,
patrol_alt
)
orbit0.name = "ORBIT 0"
orbit0.description = "Standby between this point and the next one"
orbit0.pretty_name = "Race-track start"
orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK
flight.points.append(orbit0)
orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt)
orbit1 = FlightWaypoint(
FlightWaypointType.PATROL,
orbit1p.x,
orbit1p.y,
patrol_alt
)
orbit1.name = "ORBIT 1"
orbit1.description = "Standby between this point and the previous one"
orbit1.pretty_name = "Race-track end"
orbit1.waypoint_type = FlightWaypointType.PATROL
flight.points.append(orbit1)
# Note : Targets of a PATROL TRACK waypoints are the points to be defended
@ -555,49 +594,67 @@ class FlightPlanner:
egress_heading = heading - 180 - 25
ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
ingress_point = FlightWaypoint(ingress_pos.x, ingress_pos.y, self.doctrine["INGRESS_ALT"])
ingress_point = FlightWaypoint(
FlightWaypointType.INGRESS_SEAD,
ingress_pos.x,
ingress_pos.y,
self.doctrine["INGRESS_ALT"]
)
ingress_point.name = "INGRESS"
ingress_point.pretty_name = "INGRESS on " + location.obj_name
ingress_point.description = "INGRESS on " + location.obj_name
ingress_point.waypoint_type = FlightWaypointType.INGRESS_SEAD
flight.points.append(ingress_point)
if len(custom_targets) > 0:
for target in custom_targets:
point = FlightWaypoint(target.position.x, target.position.y, 0)
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
target.position.x,
target.position.y,
0
)
point.alt_type = "RADIO"
if flight.flight_type == FlightType.DEAD:
point.description = "SEAD on " + target.type
point.pretty_name = "SEAD on " + location.obj_name
point.only_for_player = True
else:
point.description = "DEAD on " + location.obj_name
point.description = "DEAD on " + target.type
point.pretty_name = "DEAD on " + location.obj_name
point.only_for_player = True
else:
point.description = "SEAD on " + location.obj_name
point.pretty_name = "SEAD on " + location.obj_name
point.only_for_player = True
flight.points.append(point)
ingress_point.targets.append(location)
ingress_point.targetGroup = location
flight.points.append(point)
else:
point = FlightWaypoint(location.position.x, location.position.y, 0)
point = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
location.position.y,
0
)
point.alt_type = "RADIO"
if flight.flight_type == FlightType.DEAD:
point.description = "SEAD on " + location.obj_name
point.pretty_name = "SEAD on " + location.obj_name
point.only_for_player = True
else:
point.description = "DEAD on " + location.obj_name
point.pretty_name = "DEAD on " + location.obj_name
point.only_for_player = True
else:
point.description = "SEAD on " + location.obj_name
point.pretty_name = "SEAD on " + location.obj_name
point.only_for_player = True
ingress_point.targets.append(location)
ingress_point.targetGroup = location
flight.points.append(point)
egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
egress_point = FlightWaypoint(egress_pos.x, egress_pos.y, self.doctrine["EGRESS_ALT"])
egress_point = FlightWaypoint(
FlightWaypointType.EGRESS,
egress_pos.x,
egress_pos.y,
self.doctrine["EGRESS_ALT"]
)
egress_point.name = "EGRESS"
egress_point.pretty_name = "EGRESS from " + location.obj_name
egress_point.description = "EGRESS from " + location.obj_name
egress_point.waypoint_type = FlightWaypointType.EGRESS
flight.points.append(egress_point)
descend = self.generate_descend_point(flight.from_cp)
@ -628,28 +685,40 @@ class FlightPlanner:
ascend.alt = 500
flight.points.append(ascend)
ingress_point = FlightWaypoint(ingress.x, ingress.y, cap_alt)
ingress_point = FlightWaypoint(
FlightWaypointType.INGRESS_CAS,
ingress.x,
ingress.y,
cap_alt
)
ingress_point.alt_type = "RADIO"
ingress_point.name = "INGRESS"
ingress_point.pretty_name = "INGRESS"
ingress_point.description = "Ingress into CAS area"
ingress_point.waypoint_type = FlightWaypointType.INGRESS_CAS
flight.points.append(ingress_point)
center_point = FlightWaypoint(center.x, center.y, cap_alt)
center_point = FlightWaypoint(
FlightWaypointType.CAS,
center.x,
center.y,
cap_alt
)
center_point.alt_type = "RADIO"
center_point.description = "Provide CAS"
center_point.name = "CAS"
center_point.pretty_name = "CAS"
center_point.waypoint_type = FlightWaypointType.CAS
flight.points.append(center_point)
egress_point = FlightWaypoint(egress.x, egress.y, cap_alt)
egress_point = FlightWaypoint(
FlightWaypointType.EGRESS,
egress.x,
egress.y,
cap_alt
)
egress_point.alt_type = "RADIO"
egress_point.description = "Egress from CAS area"
egress_point.name = "EGRESS"
egress_point.pretty_name = "EGRESS"
egress_point.waypoint_type = FlightWaypointType.EGRESS
flight.points.append(egress_point)
descend = self.generate_descend_point(flight.from_cp)
@ -660,7 +729,6 @@ class FlightPlanner:
rtb = self.generate_rtb_waypoint(flight.from_cp)
flight.points.append(rtb)
def generate_ascend_point(self, from_cp):
"""
Generate ascend point
@ -669,15 +737,18 @@ class FlightPlanner:
"""
ascend_heading = from_cp.heading
pos_ascend = from_cp.position.point_from_heading(ascend_heading, 10000)
ascend = FlightWaypoint(pos_ascend.x, pos_ascend.y, self.doctrine["PATTERN_ALTITUDE"])
ascend = FlightWaypoint(
FlightWaypointType.ASCEND_POINT,
pos_ascend.x,
pos_ascend.y,
self.doctrine["PATTERN_ALTITUDE"]
)
ascend.name = "ASCEND"
ascend.alt_type = "RADIO"
ascend.description = "Ascend"
ascend.pretty_name = "Ascend"
ascend.waypoint_type = FlightWaypointType.ASCEND_POINT
return ascend
def generate_descend_point(self, from_cp):
"""
Generate approach/descend point
@ -686,15 +757,18 @@ class FlightPlanner:
"""
ascend_heading = from_cp.heading
descend = from_cp.position.point_from_heading(ascend_heading - 180, 10000)
descend = FlightWaypoint(descend.x, descend.y, self.doctrine["PATTERN_ALTITUDE"])
descend = FlightWaypoint(
FlightWaypointType.DESCENT_POINT,
descend.x,
descend.y,
self.doctrine["PATTERN_ALTITUDE"]
)
descend.name = "DESCEND"
descend.alt_type = "RADIO"
descend.description = "Descend to pattern alt"
descend.pretty_name = "Descend to pattern alt"
descend.waypoint_type = FlightWaypointType.DESCENT_POINT
return descend
def generate_rtb_waypoint(self, from_cp):
"""
Generate RTB landing point
@ -702,10 +776,14 @@ class FlightPlanner:
:return:
"""
rtb = from_cp.position
rtb = FlightWaypoint(rtb.x, rtb.y, 0)
rtb = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
rtb.x,
rtb.y,
0
)
rtb.name = "LANDING"
rtb.alt_type = "RADIO"
rtb.description = "RTB"
rtb.pretty_name = "RTB"
rtb.waypoint_type = FlightWaypointType.LANDING_POINT
return rtb
return rtb

View File

@ -62,6 +62,8 @@ CAP_CAPABLE = [
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
@ -130,6 +132,8 @@ CAS_CAPABLE = [
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
SpitfireLFMkIXCW,
@ -204,6 +208,8 @@ STRIKE_CAPABLE = [
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
B_17G,

View File

@ -1,10 +1,10 @@
from enum import Enum
from typing import List
from dcs.mission import StartType
from dcs.unittype import UnitType
from game import db
from dcs.unittype import UnitType
from dcs.point import MovingPoint, PointAction
from theater.controlpoint import ControlPoint
class FlightType(Enum):
@ -62,7 +62,9 @@ class PredefinedWaypointCategory(Enum):
class FlightWaypoint:
def __init__(self, x: float, y: float, alt=0):
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
alt: int = 0) -> None:
self.waypoint_type = waypoint_type
self.x = x
self.y = y
self.alt = alt
@ -73,12 +75,38 @@ class FlightWaypoint:
self.targetGroup = None
self.obj_name = ""
self.pretty_name = ""
self.waypoint_type = FlightWaypointType.TAKEOFF # type: FlightWaypointType
self.category = PredefinedWaypointCategory.NOT_PREDEFINED# type: PredefinedWaypointCategory
self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED
self.only_for_player = False
self.data = None
@classmethod
def from_pydcs(cls, point: MovingPoint,
from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(point.position.x, point.position.y,
point.alt)
waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first
# waypoint for a flight.
waypoint.waypoint_type = {
PointAction.TurningPoint: FlightWaypointType.NAV,
PointAction.FlyOverPoint: FlightWaypointType.NAV,
PointAction.FromParkingArea: FlightWaypointType.TAKEOFF,
PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF,
PointAction.FromRunway: FlightWaypointType.TAKEOFF,
}[point.action]
if waypoint.waypoint_type == FlightWaypointType.NAV:
waypoint.name = "NAV"
waypoint.pretty_name = "Nav"
waypoint.description = "Nav"
else:
waypoint.name = "TAKEOFF"
waypoint.pretty_name = "Takeoff"
waypoint.description = "Takeoff"
waypoint.description = f"Takeoff from {from_cp.name}"
return waypoint
class Flight:
unit_type: UnitType = None
from_cp = None
@ -113,10 +141,10 @@ class Flight:
# Test
if __name__ == '__main__':
from dcs.planes import A_10C
from pydcs.dcs.planes import A_10C
from theater import ControlPoint, Point, List
from_cp = ControlPoint(0, "AA", Point(0, 0), None, [], 0, 0)
f = Flight(A_10C, 4, from_cp, FlightType.CAS)
f = Flight(A_10C(), 4, from_cp, FlightType.CAS)
f.scheduled_in = 50
print(f)

View File

@ -1,4 +0,0 @@
from dcs.unitgroup import FlyingGroup

View File

@ -1,13 +1,13 @@
import logging
from dcs.statics import *
from dcs.unit import Ship, Vehicle
from game import db
from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS
from game.db import unit_type_from_name
from .airfields import RunwayData
from .conflictgen import *
from .naming import *
from dcs.mission import *
from dcs.statics import *
from .radios import RadioRegistry
from .tacan import TacanBand, TacanRegistry
FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000
@ -16,10 +16,15 @@ AA_CP_MIN_DISTANCE = 40000
class GroundObjectsGenerator:
FARP_CAPACITY = 4
def __init__(self, mission: Mission, conflict: Conflict, game):
def __init__(self, mission: Mission, conflict: Conflict, game,
radio_registry: RadioRegistry, tacan_registry: TacanRegistry):
self.m = mission
self.conflict = conflict
self.game = game
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {}
def generate_farps(self, number_of_units=1) -> typing.Collection[StaticGroup]:
if self.conflict.is_vector:
@ -103,6 +108,8 @@ class GroundObjectsGenerator:
utype = db.upgrade_to_supercarrier(utype, cp.name)
sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
atc_channel = self.radio_registry.alloc_uhf()
sg.set_frequency(atc_channel.hertz)
sg.units[0].name = self.m.string(g.units[0].name)
for i, u in enumerate(g.units):
@ -111,6 +118,8 @@ class GroundObjectsGenerator:
ship.position.x = u.position.x
ship.position.y = u.position.y
ship.heading = u.heading
# TODO: Verify.
ship.set_frequency(atc_channel.hertz)
sg.add_unit(ship)
# Find carrier direction (In the wind)
@ -125,10 +134,58 @@ class GroundObjectsGenerator:
attempt = attempt + 1
# Set UP TACAN and ICLS
modeChannel = "X" if not cp.tacanY else "Y"
sg.points[0].tasks.append(ActivateBeaconCommand(channel=cp.tacanN, modechannel=modeChannel, callsign=cp.tacanI, unit_id=sg.units[0].id, aa=False))
if ground_object.dcs_identifier == "CARRIER" and hasattr(cp, "icls"):
sg.points[0].tasks.append(ActivateICLSCommand(cp.icls, unit_id=sg.units[0].id))
tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
icls_channel = next(self.icls_alloc)
# TODO: Assign these properly.
if ground_object.dcs_identifier == "CARRIER":
tacan_callsign = random.choice([
"STE",
"CVN",
"CVH",
"CCV",
"ACC",
"ARC",
"GER",
"ABR",
"LIN",
"TRU",
])
else:
tacan_callsign = random.choice([
"LHD",
"LHA",
"LHB",
"LHC",
"LHD",
"LDS",
])
sg.points[0].tasks.append(ActivateBeaconCommand(
channel=tacan.number,
modechannel=tacan.band.value,
callsign=tacan_callsign,
unit_id=sg.units[0].id,
aa=False
))
sg.points[0].tasks.append(ActivateICLSCommand(
icls_channel,
unit_id=sg.units[0].id
))
# TODO: Make unit name usable.
# This relies on one control point mapping exactly
# to one LHA, carrier, or other usable "runway".
# This isn't wholly true, since the DD escorts of
# the carrier group are valid for helicopters, but
# they aren't exposed as such to the game. Should
# clean this up so that's possible. We can't use the
# unit name since it's an arbitrary ID.
self.runways[cp.name] = RunwayData(
cp.name,
"N/A",
atc=atc_channel,
tacan=tacan,
tacan_callsign=tacan_callsign,
icls=icls_channel,
)
else:

290
gen/kneeboard.py Normal file
View File

@ -0,0 +1,290 @@
"""Generates kneeboard pages relevant to the player's mission.
The player kneeboard includes the following information:
* Airfield (departure, arrival, divert) info.
* Flight plan (waypoint numbers, names, altitudes).
* Comm channels.
* AWACS info.
* Tanker info.
* JTAC info.
Things we should add:
* Flight plan ToT and fuel ladder (current have neither available).
* Support for planning an arrival/divert airfield separate from departure.
* Mission package infrastructure to include information about the larger
mission, i.e. information about the escort flight for a strike package.
* Target information. Steerpoints, preplanned objectives, ToT, etc.
For multiplayer missions, a kneeboard will be generated per flight.
https://forums.eagle.ru/showthread.php?t=206360 claims that kneeboard pages can
only be added per airframe, so PvP missions where each side have the same
aircraft will be able to see the enemy's kneeboard for the same airframe.
"""
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission
from dcs.unittype import FlyingType
from tabulate import tabulate
from . import units
from .aircraft import AIRCRAFT_DATA, FlightData
from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from .flights.flight import FlightWaypoint, FlightWaypointType
from .radios import RadioFrequency
class KneeboardPageWriter:
"""Creates kneeboard images."""
def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None:
self.image = Image.new('RGB', (768, 1024), (0xff, 0xff, 0xff))
# These font sizes create a relatively full page for current sorties. If
# we start generating more complicated flight plans, or start including
# more information in the comm ladder (the latter of which we should
# probably do), we'll need to split some of this information off into a
# second page.
self.title_font = ImageFont.truetype("arial.ttf", 32)
self.heading_font = ImageFont.truetype("arial.ttf", 24)
self.content_font = ImageFont.truetype("arial.ttf", 20)
self.table_font = ImageFont.truetype(
"resources/fonts/Inconsolata.otf", 20)
self.draw = ImageDraw.Draw(self.image)
self.x = page_margin
self.y = page_margin
self.line_spacing = line_spacing
@property
def position(self) -> Tuple[int, int]:
return self.x, self.y
def text(self, text: str, font=None,
fill: Tuple[int, int, int] = (0, 0, 0)) -> None:
if font is None:
font = self.content_font
self.draw.text(self.position, text, font=font, fill=fill)
width, height = self.draw.textsize(text, font=font)
self.y += height + self.line_spacing
def title(self, title: str) -> None:
self.text(title, font=self.title_font)
def heading(self, text: str) -> None:
self.text(text, font=self.heading_font)
def table(self, cells: List[List[str]],
headers: Optional[List[str]] = None) -> None:
table = tabulate(cells, headers=headers, numalign="right")
self.text(table, font=self.table_font)
def write(self, path: Path) -> None:
self.image.save(path)
class KneeboardPage:
"""Base class for all kneeboard pages."""
def write(self, path: Path) -> None:
"""Writes the kneeboard page to the given path."""
raise NotImplementedError
@dataclass(frozen=True)
class NumberedWaypoint:
number: int
waypoint: FlightWaypoint
class FlightPlanBuilder:
def __init__(self) -> None:
self.rows: List[List[str]] = []
self.target_points: List[NumberedWaypoint] = []
def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None:
if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT:
self.target_points.append(NumberedWaypoint(waypoint_num, waypoint))
return
if self.target_points:
self.coalesce_target_points()
self.target_points = []
self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint))
def coalesce_target_points(self) -> None:
if len(self.target_points) <= 4:
for steerpoint in self.target_points:
self.add_waypoint_row(steerpoint)
return
first_waypoint_num = self.target_points[0].number
last_waypoint_num = self.target_points[-1].number
self.rows.append([
f"{first_waypoint_num}-{last_waypoint_num}",
"Target points",
"0"
])
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
self.rows.append([
waypoint.number,
waypoint.waypoint.pretty_name,
str(int(units.meters_to_feet(waypoint.waypoint.alt)))
])
def build(self) -> List[List[str]]:
return self.rows
class BriefingPage(KneeboardPage):
"""A kneeboard page containing briefing information."""
def __init__(self, flight: FlightData, comms: List[CommInfo],
awacs: List[AwacsInfo], tankers: List[TankerInfo],
jtacs: List[JtacInfo]) -> None:
self.flight = flight
self.comms = list(comms)
self.awacs = awacs
self.tankers = tankers
self.jtacs = jtacs
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
def write(self, path: Path) -> None:
writer = KneeboardPageWriter()
writer.title(f"{self.flight.callsign} Mission Info")
# TODO: Handle carriers.
writer.heading("Airfield Info")
writer.table([
self.airfield_info_row("Departure", self.flight.departure),
self.airfield_info_row("Arrival", self.flight.arrival),
self.airfield_info_row("Divert", self.flight.divert),
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"])
writer.heading("Flight Plan")
flight_plan_builder = FlightPlanBuilder()
for num, waypoint in enumerate(self.flight.waypoints):
flight_plan_builder.add_waypoint(num, waypoint)
writer.table(flight_plan_builder.build(),
headers=["STPT", "Action", "Alt"])
writer.heading("Comm Ladder")
comms = []
for comm in self.comms:
comms.append([comm.name, self.format_frequency(comm.freq)])
writer.table(comms, headers=["Name", "UHF"])
writer.heading("AWACS")
awacs = []
for a in self.awacs:
awacs.append([a.callsign, self.format_frequency(a.freq)])
writer.table(awacs, headers=["Callsign", "UHF"])
writer.heading("Tankers")
tankers = []
for tanker in self.tankers:
tankers.append([
tanker.callsign,
tanker.variant,
tanker.tacan,
self.format_frequency(tanker.freq),
])
writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"])
writer.heading("JTAC")
jtacs = []
for jtac in self.jtacs:
jtacs.append([jtac.callsign, jtac.region, jtac.code])
writer.table(jtacs, headers=["Callsign", "Region", "Laser Code"])
writer.write(path)
def airfield_info_row(self, row_title: str,
runway: Optional[RunwayData]) -> List[str]:
"""Creates a table row for a given airfield.
Args:
row_title: Purpose of the airfield. e.g. "Departure", "Arrival" or
"Divert".
runway: The runway described by this row.
Returns:
A list of strings to be used as a row of the airfield table.
"""
if runway is None:
return [row_title, "", "", "", "", ""]
atc = ""
if runway.atc is not None:
atc = self.format_frequency(runway.atc)
return [
row_title,
runway.airfield_name,
atc,
runway.tacan or "",
runway.ils or runway.icls or "",
runway.runway_name,
]
def format_frequency(self, frequency: RadioFrequency) -> str:
channel = self.flight.channel_for(frequency)
if channel is None:
return str(frequency)
namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
channel_name = namer.channel_name(channel.radio_id, channel.channel)
return f"{channel_name} {frequency}"
class KneeboardGenerator(MissionInfoGenerator):
"""Creates kneeboard pages for each client flight in the mission."""
def __init__(self, mission: Mission) -> None:
super().__init__(mission)
def generate(self) -> None:
"""Generates a kneeboard per client flight."""
temp_dir = Path("kneeboards")
temp_dir.mkdir(exist_ok=True)
for aircraft, pages in self.pages_by_airframe().items():
aircraft_dir = temp_dir / aircraft.id
aircraft_dir.mkdir(exist_ok=True)
for idx, page in enumerate(pages):
page_path = aircraft_dir / f"page{idx:02}.png"
page.write(page_path)
self.mission.add_aircraft_kneeboard(aircraft, page_path)
def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]:
"""Returns a list of kneeboard pages per airframe in the mission.
Only client flights will be included, but because DCS does not support
group-specific kneeboard pages, flights (possibly from opposing sides)
will be able to see the kneeboards of all aircraft of the same type.
Returns:
A dict mapping aircraft types to the list of kneeboard pages for
that aircraft.
"""
all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list)
for flight in self.flights:
if not flight.client_units:
continue
all_flights[flight.aircraft_type].extend(
self.generate_flight_kneeboard(flight))
return all_flights
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
"""Returns a list of kneeboard pages for the given flight."""
return [
BriefingPage(
flight, self.comms, self.awacs, self.tankers, self.jtacs
),
]

226
gen/radios.py Normal file
View File

@ -0,0 +1,226 @@
"""Radio frequency types and allocators."""
import itertools
from dataclasses import dataclass
from typing import Dict, Iterator, List, Set
@dataclass(frozen=True)
class RadioFrequency:
"""A radio frequency.
Not currently concerned with tracking modulation, just the frequency.
"""
#: The frequency in kilohertz.
hertz: int
def __str__(self):
if self.hertz >= 1000000:
return self.format("MHz", 1000000)
return self.format("kHz", 1000)
def format(self, units: str, divisor: int) -> str:
converted = self.hertz / divisor
if converted.is_integer():
return f"{int(converted)} {units}"
return f"{converted:0.3f} {units}"
@property
def mhz(self) -> float:
"""Returns the frequency in megahertz.
Returns:
The frequency in megahertz.
"""
return self.hertz / 1000000
def MHz(num: int, khz: int = 0) -> RadioFrequency:
return RadioFrequency(num * 1000000 + khz * 1000)
def kHz(num: int) -> RadioFrequency:
return RadioFrequency(num * 1000)
@dataclass(frozen=True)
class Radio:
"""A radio.
Defines the minimum (inclusive) and maximum (exclusive) range of the radio.
"""
#: The name of the radio.
name: str
#: The minimum (inclusive) frequency tunable by this radio.
minimum: RadioFrequency
#: The maximum (exclusive) frequency tunable by this radio.
maximum: RadioFrequency
#: The spacing between adjacent frequencies.
step: RadioFrequency
def __str__(self) -> str:
return self.name
def range(self) -> Iterator[RadioFrequency]:
"""Returns an iterator over the usable frequencies of this radio."""
return (RadioFrequency(x) for x in range(
self.minimum.hertz, self.maximum.hertz, self.step.hertz
))
class OutOfChannelsError(RuntimeError):
"""Raised when all channels usable by this radio have been allocated."""
def __init__(self, radio: Radio) -> None:
super().__init__(f"No available channels for {radio}")
class ChannelInUseError(RuntimeError):
"""Raised when attempting to reserve an in-use frequency."""
def __init__(self, frequency: RadioFrequency) -> None:
super().__init__(f"{frequency} is already in use")
# 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", MHz(225), MHz(400), step=MHz(1)),
Radio("AN/ARC-186(V) AM", MHz(116), MHz(152), step=MHz(1)),
Radio("AN/ARC-186(V) FM", MHz(30), MHz(76), step=MHz(1)),
# The AN/ARC-210 can also use [30, 88) and [108, 118), but the current
# implementation can't implement the gap and the radio can't transmit on the
# latter. There's still plenty of channels between 118 MHz and 400 MHz, so
# not worth worrying about.
Radio("AN/ARC-210", MHz(118), MHz(400), step=MHz(1)),
Radio("AN/ARC-222", MHz(116), MHz(174), step=MHz(1)),
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
# can model gaps later if needed.
Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)),
Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)),
# Tomcat radios
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)),
# AN/ARC-182 can also operate from 30 MHz to 88 MHz, as well as from 225 MHz
# to 400 MHz range, but we can't model gaps with the current implementation.
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)),
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps.
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
# P-51 / P-47 Radio
# 4 preset channels (A/B/C/D)
Radio("SCR522", MHz(100), MHz(156), step=kHz(25)),
Radio("R&S M3AR VHF", MHz(108), MHz(174), step=MHz(1)),
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
]
def get_radio(name: str) -> Radio:
"""Returns the radio with the given name.
Args:
name: Name of the radio to return.
Returns:
The radio matching name.
Raises:
KeyError: No matching radio was found.
"""
for radio in RADIOS:
if radio.name == name:
return radio
raise KeyError
class RadioRegistry:
"""Manages allocation of radio channels.
There's some room for improvement here. We could prefer to allocate
frequencies that are available to the fewest number of radios first, so
radios with wide bands like the AN/ARC-210 don't exhaust all the channels
available to narrower radios like the AN/ARC-186(V). In practice there are
probably plenty of channels, so we can deal with that later if we need to.
We could also allocate using a larger increment, returning to smaller
increments each time the range is exhausted. This would help with the
previous problem, as the AN/ARC-186(V) would still have plenty of 25 kHz
increment channels left after the AN/ARC-210 moved on to the higher
frequencies. This would also look a little nicer than having every flight
allocated in the 30 MHz range.
"""
# Not a real radio, but useful for allocating a channel usable for
# inter-flight communications.
BLUFOR_UHF = Radio("BLUFOR UHF", MHz(225), MHz(400), step=MHz(1))
def __init__(self) -> None:
self.allocated_channels: Set[RadioFrequency] = set()
self.radio_allocators: Dict[Radio, Iterator[RadioFrequency]] = {}
radios = itertools.chain(RADIOS, [self.BLUFOR_UHF])
for radio in radios:
self.radio_allocators[radio] = radio.range()
def alloc_for_radio(self, radio: Radio) -> RadioFrequency:
"""Allocates a radio channel tunable by the given radio.
Args:
radio: The radio to allocate a channel for.
Returns:
A radio channel compatible with the given radio.
Raises:
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:
pass
self.reserve(channel)
return channel
except StopIteration:
raise OutOfChannelsError(radio)
def alloc_uhf(self) -> RadioFrequency:
"""Allocates a UHF radio channel suitable for inter-flight comms.
Returns:
A UHF radio channel suitable for inter-flight comms.
Raises:
OutOfChannelsError: All channels compatible with the given radio are
already allocated.
"""
return self.alloc_for_radio(self.BLUFOR_UHF)
def reserve(self, frequency: RadioFrequency) -> None:
"""Reserves the given channel.
Reserving a channel ensures that it will not be allocated in the future.
Args:
frequency: The channel to reserve.
Raises:
ChannelInUseError: The given frequency is already in use.
"""
if frequency in self.allocated_channels:
raise ChannelInUseError(frequency)
self.allocated_channels.add(frequency)

View File

@ -10,9 +10,12 @@ class BoforsGenerator(GroupGenerator):
This generate a Bofors flak artillery group
"""
name = "Bofors AAA"
price = 75
def generate(self):
grid_x = random.randint(2, 4)
grid_y = random.randint(2, 4)
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10,40)

View File

@ -11,11 +11,14 @@ class FlakGenerator(GroupGenerator):
This generate a German flak artillery group
"""
def generate(self):
grid_x = random.randint(2, 4)
grid_y = random.randint(2, 4)
name = "Flak Site"
price = 135
spacing = random.randint(30,60)
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(30, 60)
index = 0
mixed = random.choice([True, False])
@ -32,7 +35,7 @@ class FlakGenerator(GroupGenerator):
unit_type = random.choice(GFLAK)
# Search lights
search_pos = self.get_circular_position(random.randint(2,5), 90)
search_pos = self.get_circular_position(random.randint(2,3), 90)
for index, pos in enumerate(search_pos):
self.add_unit(AirDefence.Flak_Searchlight_37, "SearchLight#" + str(index), pos[0], pos[1], self.heading)

View File

@ -10,9 +10,12 @@ class ZU23InsurgentGenerator(GroupGenerator):
This generate a ZU23 insurgent flak artillery group
"""
name = "Zu-23 Site"
price = 56
def generate(self):
grid_x = random.randint(2, 4)
grid_y = random.randint(2, 4)
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10,40)

View File

@ -10,6 +10,9 @@ class AvengerGenerator(GroupGenerator):
This generate an Avenger group
"""
name = "Avenger Group"
price = 62
def generate(self):
num_launchers = random.randint(2, 3)

View File

@ -10,6 +10,9 @@ class ChaparralGenerator(GroupGenerator):
This generate a Chaparral group
"""
name = "Chaparral Group"
price = 66
def generate(self):
num_launchers = random.randint(2, 4)

View File

@ -10,6 +10,9 @@ class GepardGenerator(GroupGenerator):
This generate a Gepard group
"""
name = "Gepard Group"
price = 50
def generate(self):
self.add_unit(AirDefence.SPAAA_Gepard, "SPAAA", self.position.x, self.position.y, self.heading)
if random.randint(0, 1) == 1:

View File

@ -1,5 +1,7 @@
import random
from typing import List
from dcs.unittype import UnitType
from dcs.vehicles import AirDefence
from game import db
@ -99,6 +101,23 @@ SAM_PRICES = {
AirDefence.HQ_7_Self_Propelled_LN: 35
}
def get_faction_possible_sams_units(faction: str) -> List[UnitType]:
"""
Return the list
:param faction: Faction to search units for
"""
return [u for u in db.FACTIONS[faction]["units"] if u in AirDefence.__dict__.values()]
def get_faction_possible_sams_generator(faction: str) -> List[UnitType]:
"""
Return the list of possible SAM generator for the given faction
:param faction: Faction to search units for
"""
return [SAM_MAP[u] for u in get_faction_possible_sams_units(faction)]
def generate_anti_air_group(game, parent_cp, ground_object, faction:str):
"""
This generate a SAM group
@ -107,7 +126,7 @@ def generate_anti_air_group(game, parent_cp, ground_object, faction:str):
:param country: Owner country
:return: Nothing, but put the group reference inside the ground object
"""
possible_sams = [u for u in db.FACTIONS[faction]["units"] if u in AirDefence.__dict__.values()]
possible_sams = get_faction_possible_sams_units(faction)
if len(possible_sams) > 0:
sam = random.choice(possible_sams)
generator = SAM_MAP[sam](game, ground_object)

View File

@ -10,6 +10,9 @@ class HawkGenerator(GroupGenerator):
This generate an HAWK group
"""
name = "Hawk Site"
price = 115
def generate(self):
self.add_unit(AirDefence.SAM_Hawk_PCP, "PCP", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Hawk_SR_AN_MPQ_50, "SR", self.position.x + 20, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class HQ7Generator(GroupGenerator):
This generate an HQ7 group
"""
name = "HQ-7 Site"
price = 120
def generate(self):
self.add_unit(AirDefence.HQ_7_Self_Propelled_STR, "STR", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN", self.position.x + 20, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class LinebackerGenerator(GroupGenerator):
This generate an m6 linebacker group
"""
name = "Linebacker Group"
price = 75
def generate(self):
num_launchers = random.randint(2, 4)

View File

@ -10,6 +10,9 @@ class PatriotGenerator(GroupGenerator):
This generate a Patriot group
"""
name = "Patriot Battery"
price = 240
def generate(self):
# Command Post
self.add_unit(AirDefence.SAM_Patriot_AMG_AN_MRC_137, "MRC", self.position.x, self.position.y, self.heading)
@ -18,13 +21,13 @@ class PatriotGenerator(GroupGenerator):
self.add_unit(AirDefence.SAM_Patriot_EPP_III, "EPP", self.position.x, self.position.y + 30, self.heading)
self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "ICC", self.position.x + 30, self.position.y + 30, self.heading)
num_launchers = random.randint(2, 4)
num_launchers = random.randint(3, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Patriot_LN_M901, "LN#" + str(i), position[0], position[1], position[2])
# Short range protection for high value site
num_launchers = random.randint(2, 4)
num_launchers = random.randint(3, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])

View File

@ -10,6 +10,9 @@ class RapierGenerator(GroupGenerator):
This generate a Rapier Group
"""
name = "Rapier AA Site"
price = 50
def generate(self):
self.add_unit(AirDefence.Rapier_FSA_Blindfire_Tracker, "BT", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.Rapier_FSA_Optical_Tracker, "OT", self.position.x + 20, self.position.y, self.heading)

View File

@ -8,6 +8,9 @@ class RolandGenerator(GroupGenerator):
This generate a Roland group
"""
name = "Roland Site"
price = 40
def generate(self):
self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Roland_EWR, "EWR", self.position.x + 40, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class SA10Generator(GroupGenerator):
This generate a SA-10 group
"""
name = "SA-10/S-300PS Battery"
price = 450
def generate(self):
# Command Post
self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class SA11Generator(GroupGenerator):
This generate a SA-11 group
"""
name = "SA-11 Buk Battery"
price = 180
def generate(self):
self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x+20, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class SA13Generator(GroupGenerator):
This generate a SA-13 group
"""
name = "SA-13 Strela Group"
price = 50
def generate(self):
self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x+40, self.position.y, self.heading)

View File

@ -8,6 +8,9 @@ class SA15Generator(GroupGenerator):
This generate a SA-15 group
"""
name = "SA-15 Tor Group"
price = 55
def generate(self):
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "ADS", self.position.x, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_UAZ_469, "EWR", self.position.x + 40, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class SA19Generator(GroupGenerator):
This generate a SA-19 group
"""
name = "SA-19 Tunguska Group"
price = 90
def generate(self):
num_launchers = random.randint(1, 3)

View File

@ -10,6 +10,9 @@ class SA2Generator(GroupGenerator):
This generate a SA-2 group
"""
name = "SA-2/S-75 Site"
price = 74
def generate(self):
self.add_unit(AirDefence.SAM_SR_P_19, "SR", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song, "TR", self.position.x + 20, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class SA3Generator(GroupGenerator):
This generate a SA-3 group
"""
name = "SA-3/S-125 Site"
price = 80
def generate(self):
self.add_unit(AirDefence.SAM_SR_P_19, "SR", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_3_S_125_TR_SNR, "TR", self.position.x + 20, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class SA6Generator(GroupGenerator):
This generate a SA-6 group
"""
name = "SA-6 Kub Site"
price = 102
def generate(self):
self.add_unit(AirDefence.SAM_SA_6_Kub_STR_9S91, "STR", self.position.x, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class SA8Generator(GroupGenerator):
This generate a SA-8 group
"""
name = "SA-8 OSA Site"
price = 55
def generate(self):
self.add_unit(AirDefence.SAM_SA_8_Osa_9A33, "OSA", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_8_Osa_LD_9T217, "LD", self.position.x + 20, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class SA9Generator(GroupGenerator):
This generate a SA-9 group
"""
name = "SA-9 Group"
price = 40
def generate(self):
self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x+40, self.position.y, self.heading)

View File

@ -10,6 +10,9 @@ class VulcanGenerator(GroupGenerator):
This generate a Vulcan group
"""
name = "Vulcan Group"
price = 25
def generate(self):
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA", self.position.x, self.position.y, self.heading)
if random.randint(0, 1) == 1:

View File

@ -10,8 +10,11 @@ class ZSU23Generator(GroupGenerator):
This generate a ZSU 23 group
"""
name = "ZSU-23 Group"
price = 50
def generate(self):
num_launchers = random.randint(2, 5)
num_launchers = random.randint(4, 5)
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
for i, position in enumerate(positions):

View File

@ -10,9 +10,12 @@ class ZU23Generator(GroupGenerator):
This generate a ZU23 flak artillery group
"""
name = "ZU-23 Group"
price = 54
def generate(self):
grid_x = random.randint(2, 4)
grid_y = random.randint(2, 4)
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10,40)

View File

@ -10,6 +10,9 @@ class ZU23UralGenerator(GroupGenerator):
This generate a Zu23 Ural group
"""
name = "ZU-23 Ural Group"
price = 64
def generate(self):
num_launchers = random.randint(2, 8)

View File

@ -10,6 +10,9 @@ class ZU23UralInsurgentGenerator(GroupGenerator):
This generate a Zu23 Ural group
"""
name = "ZU-23 Ural Insurgent Group"
price = 64
def generate(self):
num_launchers = random.randint(2, 8)

83
gen/tacan.py Normal file
View File

@ -0,0 +1,83 @@
"""TACAN channel handling."""
from dataclasses import dataclass
from enum import Enum
from typing import Dict, Iterator, Set
class TacanBand(Enum):
X = "X"
Y = "Y"
def range(self) -> Iterator["TacanChannel"]:
"""Returns an iterator over the channels in this band."""
return (TacanChannel(x, self) for x in range(1, 100))
@dataclass(frozen=True)
class TacanChannel:
number: int
band: TacanBand
def __str__(self) -> str:
return f"{self.number}{self.band.value}"
class OutOfTacanChannelsError(RuntimeError):
"""Raised when all channels in this band have been allocated."""
def __init__(self, band: TacanBand) -> None:
super().__init__(f"No available channels in TACAN {band.value} band")
class TacanChannelInUseError(RuntimeError):
"""Raised when attempting to reserve an in-use channel."""
def __init__(self, channel: TacanChannel) -> None:
super().__init__(f"{channel} is already in use")
class TacanRegistry:
"""Manages allocation of TACAN channels."""
def __init__(self) -> None:
self.allocated_channels: Set[TacanChannel] = set()
self.band_allocators: Dict[TacanBand, Iterator[TacanChannel]] = {}
for band in TacanBand:
self.band_allocators[band] = band.range()
def alloc_for_band(self, band: TacanBand) -> TacanChannel:
"""Allocates a TACAN channel in the given band.
Args:
band: The TACAN band to allocate a channel for.
Returns:
A TACAN channel in the given band.
Raises:
OutOfChannelsError: All channels compatible with the given radio are
already allocated.
"""
allocator = self.band_allocators[band]
try:
while (channel := next(allocator)) in self.allocated_channels:
pass
return channel
except StopIteration:
raise OutOfTacanChannelsError(band)
def reserve(self, channel: TacanChannel) -> None:
"""Reserves the given channel.
Reserving a channel ensures that it will not be allocated in the future.
Args:
channel: The channel to reserve.
Raises:
ChannelInUseError: The given frequency is already in use.
"""
if channel in self.allocated_channels:
raise TacanChannelInUseError(channel)
self.allocated_channels.add(channel)

6
gen/units.py Normal file
View File

@ -0,0 +1,6 @@
"""Unit conversions."""
def meters_to_feet(meters: float) -> float:
"""Convers meters to feet."""
return meters * 3.28084

2
pydcs

@ -1 +1 @@
Subproject commit dcc3d846316af2925c93ae09840c3ab4a1150e59
Subproject commit f46781b854102a9f06948c8fb81a40331b78459e

View File

@ -4,7 +4,7 @@ import logging
import os
import sys
from pydcs import dcs
import dcs
from PySide2 import QtWidgets
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QApplication, QSplashScreen

View File

@ -8,7 +8,7 @@ from game.event import UnitsDeliveryEvent, FrontlineAttackEvent
from theater.theatergroundobject import CATEGORY_MAP
from userdata.liberation_theme import get_theme_icons
VERSION_STRING = "2.1.0"
VERSION_STRING = "2.1.1"
URLS : Dict[str, str] = {
"Manual": "https://github.com/khopa/dcs_liberation/wiki",

View File

@ -21,6 +21,7 @@ class QTopPanel(QFrame):
self.setMaximumHeight(70)
self.init_ui()
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update)
def init_ui(self):
@ -101,4 +102,7 @@ class QTopPanel(QFrame):
def proceed(self):
self.subwindow = QMissionPlanning(self.game)
self.subwindow.show()
self.subwindow.show()
def budget_update(self, game:Game):
self.budgetBox.setGame(game)

View File

@ -35,6 +35,7 @@ class QFilteredComboBox(QComboBox):
super(QFilteredComboBox, self).setModel(model)
self.pFilterModel.setSourceModel(model)
self.completer.setModel(self.pFilterModel)
self.model().sort(0)
def setModelColumn(self, column):
self.completer.setCompletionColumn(column)

View File

@ -57,13 +57,16 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
enemy_cp = [ecp for ecp in cp.connected_points if ecp.captured != cp.captured]
for ecp in enemy_cp:
pos = Conflict.frontline_position(self.game.theater, cp, ecp)[0]
wpt = FlightWaypoint(pos.x, pos.y, 800)
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
pos.x,
pos.y,
800)
wpt.name = "Frontline " + cp.name + "/" + ecp.name + " [CAS]"
wpt.alt_type = "RADIO"
wpt.pretty_name = wpt.name
wpt.description = "Frontline"
wpt.data = [cp, ecp]
wpt.waypoint_type = FlightWaypointType.CUSTOM
wpt.category = PredefinedWaypointCategory.FRONTLINE
i = add_model_item(i, model, wpt.pretty_name, wpt)
@ -72,14 +75,18 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured):
for ground_object in cp.ground_objects:
if not ground_object.is_dead and not ground_object.dcs_identifier == "AA":
wpt = FlightWaypoint(ground_object.position.x,ground_object.position.y, 0)
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
ground_object.position.x,
ground_object.position.y,
0
)
wpt.alt_type = "RADIO"
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + ground_object.category + " #" + str(ground_object.object_id)
wpt.pretty_name = wpt.name
wpt.obj_name = ground_object.obj_name
wpt.targets.append(ground_object)
wpt.data = ground_object
wpt.waypoint_type = FlightWaypointType.CUSTOM
if cp.captured:
wpt.description = "Friendly Building"
wpt.category = PredefinedWaypointCategory.ALLY_BUILDING
@ -95,7 +102,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if not ground_object.is_dead and ground_object.dcs_identifier == "AA":
for g in ground_object.groups:
for j, u in enumerate(g.units):
wpt = FlightWaypoint(u.position.x, u.position.y, 0)
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
u.position.x,
u.position.y,
0
)
wpt.alt_type = "RADIO"
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j)
wpt.pretty_name = wpt.name
@ -114,11 +126,15 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if self.include_airbases:
for cp in self.game.theater.controlpoints:
if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured):
wpt = FlightWaypoint(cp.position.x, cp.position.y, 0)
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
cp.position.x,
cp.position.y,
0
)
wpt.alt_type = "RADIO"
wpt.name = cp.name
wpt.data = cp
wpt.waypoint_type = FlightWaypointType.CUSTOM
if cp.captured:
wpt.description = "Position of " + cp.name + " [Friendly Airbase]"
wpt.category = PredefinedWaypointCategory.ALLY_CP
@ -133,7 +149,6 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
else:
wpt.pretty_name = cp.name + " (Airbase)"
i = add_model_item(i, model, wpt.pretty_name, wpt)
self.setModel(model)

View File

@ -16,6 +16,7 @@ class GameUpdateSignal(QObject):
instance = None
gameupdated = Signal(Game)
budgetupdated = Signal(Game)
debriefingReceived = Signal(DebriefingSignal)
def __init__(self):
@ -25,6 +26,9 @@ class GameUpdateSignal(QObject):
def updateGame(self, game: Game):
self.gameupdated.emit(game)
def updateBudget(self, game: Game):
self.budgetupdated.emit(game)
def sendDebriefing(self, game: Game, gameEvent: Event, debriefing: Debriefing):
sig = DebriefingSignal(game, gameEvent, debriefing)
self.gameupdated.emit(game)

View File

@ -29,7 +29,7 @@ class QLiberationWindow(QMainWindow):
self.setGame(persistency.restore_game())
self.setGeometry(300, 100, 270, 100)
self.setWindowTitle("DCS Liberation")
self.setWindowTitle("DCS Liberation - v" + CONST.VERSION_STRING)
self.setWindowIcon(QIcon("./resources/icon.png"))
self.statusBar().showMessage('Ready')
@ -69,27 +69,31 @@ class QLiberationWindow(QMainWindow):
GameUpdateSignal.get_instance().debriefingReceived.connect(self.onDebriefing)
def initActions(self):
self.newGameAction = QAction("New Game", self)
self.newGameAction = QAction("&New Game", self)
self.newGameAction.setIcon(QIcon(CONST.ICONS["New"]))
self.newGameAction.triggered.connect(self.newGame)
self.newGameAction.setShortcut('CTRL+N')
self.openAction = QAction("Open", self)
self.openAction = QAction("&Open", self)
self.openAction.setIcon(QIcon(CONST.ICONS["Open"]))
self.openAction.triggered.connect(self.openFile)
self.openAction.setShortcut('CTRL+O')
self.saveGameAction = QAction("Save", self)
self.saveGameAction = QAction("&Save", self)
self.saveGameAction.setIcon(QIcon(CONST.ICONS["Save"]))
self.saveGameAction.triggered.connect(self.saveGame)
self.saveGameAction.setShortcut('CTRL+S')
self.saveAsAction = QAction("Save As", self)
self.saveAsAction = QAction("Save &As", self)
self.saveAsAction.setIcon(QIcon(CONST.ICONS["Save"]))
self.saveAsAction.triggered.connect(self.saveGameAs)
self.saveAsAction.setShortcut('CTRL+A')
self.showAboutDialogAction = QAction("About DCS Liberation", self)
self.showAboutDialogAction = QAction("&About DCS Liberation", self)
self.showAboutDialogAction.setIcon(QIcon.fromTheme("help-about"))
self.showAboutDialogAction.triggered.connect(self.showAboutDialog)
self.showLiberationPrefDialogAction = QAction("Preferences", self)
self.showLiberationPrefDialogAction = QAction("&Preferences", self)
self.showLiberationPrefDialogAction.setIcon(QIcon.fromTheme("help-about"))
self.showLiberationPrefDialogAction.triggered.connect(self.showLiberationDialog)
@ -102,57 +106,47 @@ class QLiberationWindow(QMainWindow):
def initMenuBar(self):
self.menu = self.menuBar()
file_menu = self.menu.addMenu("File")
file_menu = self.menu.addMenu("&File")
file_menu.addAction(self.newGameAction)
file_menu.addAction(self.openAction)
file_menu.addSeparator()
file_menu.addAction(self.saveGameAction)
file_menu.addAction(self.saveAsAction)
file_menu.addSeparator()
file_menu.addAction(self.showLiberationPrefDialogAction)
file_menu.addSeparator()
#file_menu.addAction("Close Current Game", lambda: self.closeGame()) # Not working
file_menu.addAction("Exit" , lambda: self.exit())
file_menu.addAction("E&xit" , lambda: self.exit())
help_menu = self.menu.addMenu("Help")
help_menu.addAction("Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
help_menu.addAction("Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation"))
help_menu.addAction("Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases"))
help_menu.addAction("Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"]))
help_menu.addAction("ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"]))
help_menu.addAction("Report an issue", lambda: webbrowser.open_new_tab(URLS["Issues"]))
displayMenu = self.menu.addMenu("&Display")
help_menu.addSeparator()
help_menu.addAction(self.showAboutDialogAction)
displayMenu = self.menu.addMenu("Display")
tg_cp_visibility = QAction('Control Point', displayMenu)
tg_cp_visibility = QAction('&Control Point', displayMenu)
tg_cp_visibility.setCheckable(True)
tg_cp_visibility.setChecked(True)
tg_cp_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("cp", tg_cp_visibility.isChecked()))
tg_go_visibility = QAction('Ground Objects', displayMenu)
tg_go_visibility = QAction('&Ground Objects', displayMenu)
tg_go_visibility.setCheckable(True)
tg_go_visibility.setChecked(True)
tg_go_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("go", tg_go_visibility.isChecked()))
tg_line_visibility = QAction('Lines', displayMenu)
tg_line_visibility = QAction('&Lines', displayMenu)
tg_line_visibility.setCheckable(True)
tg_line_visibility.setChecked(True)
tg_line_visibility.toggled.connect(
lambda: QLiberationMap.set_display_rule("lines", tg_line_visibility.isChecked()))
tg_event_visibility = QAction('Events', displayMenu)
tg_event_visibility = QAction('&Events', displayMenu)
tg_event_visibility.setCheckable(True)
tg_event_visibility.setChecked(True)
tg_event_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("events", tg_event_visibility.isChecked()))
tg_sam_visibility = QAction('SAM Range', displayMenu)
tg_sam_visibility = QAction('&SAM Range', displayMenu)
tg_sam_visibility.setCheckable(True)
tg_sam_visibility.setChecked(True)
tg_sam_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("sam", tg_sam_visibility.isChecked()))
tg_flight_path_visibility = QAction('Flight Paths', displayMenu)
tg_flight_path_visibility = QAction('&Flight Paths', displayMenu)
tg_flight_path_visibility.setCheckable(True)
tg_flight_path_visibility.setChecked(False)
tg_flight_path_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("flight_paths", tg_flight_path_visibility.isChecked()))
@ -164,6 +158,17 @@ class QLiberationWindow(QMainWindow):
displayMenu.addAction(tg_sam_visibility)
displayMenu.addAction(tg_flight_path_visibility)
help_menu = self.menu.addMenu("&Help")
help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
help_menu.addAction("&Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation"))
help_menu.addAction("&Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases"))
help_menu.addAction("&Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"]))
help_menu.addAction("&ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"]))
help_menu.addAction("Report an &issue", lambda: webbrowser.open_new_tab(URLS["Issues"]))
help_menu.addSeparator()
help_menu.addAction(self.showAboutDialogAction)
def newGame(self):
wizard = NewGameWizard(self)
wizard.show()
@ -216,7 +221,7 @@ class QLiberationWindow(QMainWindow):
"<h4>Authors</h4>" + \
"<p>DCS Liberation was originally developed by <b>shdwp</b>, DCS Liberation 2.0 is a partial rewrite based on this work by <b>Khopa</b>." \
"<h4>Contributors</h4>" + \
"shdwp, Khopa, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody" + \
"shdwp, Khopa, ColonelPanic, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl" + \
"<h4>Special Thanks :</h4>" \
"<b>rp-</b> <i>for the pydcs framework</i><br/>"\
"<b>Grimes (mrSkortch)</b> & <b>Speed</b> <i>for the MIST framework</i><br/>"\

View File

@ -1,4 +1,5 @@
from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout
from qt_ui.uiconstants import VEHICLES_ICONS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
@ -7,19 +8,40 @@ from theater import ControlPoint, TheaterGroundObject
class QBaseDefenseGroupInfo(QGroupBox):
def __init__(self, cp:ControlPoint, ground_object: TheaterGroundObject, game):
def __init__(self, cp: ControlPoint, ground_object: TheaterGroundObject, game):
super(QBaseDefenseGroupInfo, self).__init__("Group : " + ground_object.obj_name)
self.ground_object = ground_object
self.cp = cp
self.game = game
self.buildings = game.theater.find_ground_objects_by_obj_name(self.ground_object.obj_name)
self.main_layout = QVBoxLayout()
self.unit_layout = QGridLayout()
self.init_ui()
def init_ui(self):
self.buildLayout()
manage_button = QPushButton("Manage")
manage_button.setProperty("style", "btn-success")
manage_button.setMaximumWidth(180)
manage_button.clicked.connect(self.onManage)
self.main_layout.addLayout(self.unit_layout)
self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft)
self.setLayout(self.main_layout)
def buildLayout(self):
unit_dict = {}
layout = QGridLayout()
for i in range(self.unit_layout.rowCount()):
for j in range(self.unit_layout.columnCount()):
item = self.unit_layout.itemAtPosition(i, j)
if item is not None and item.widget() is not None:
item.widget().setParent(None)
print("Remove " + str(i) + ", " + str(j))
for g in self.ground_object.groups:
for u in g.units:
if u.type in unit_dict.keys():
@ -28,25 +50,27 @@ class QBaseDefenseGroupInfo(QGroupBox):
unit_dict[u.type] = 1
i = 0
for k, v in unit_dict.items():
#icon = QLabel()
#if k in VEHICLES_ICONS.keys():
# icon.setPixmap(VEHICLES_ICONS[k])
#else:
# icon.setText("<b>" + k[:6] + "</b>")
#icon.setProperty("style", "icon-plane")
#layout.addWidget(icon, i, 0)
layout.addWidget(QLabel(str(v) + " x " + "<strong>" + k + "</strong>"), i, 0)
icon = QLabel()
if k in VEHICLES_ICONS.keys():
icon.setPixmap(VEHICLES_ICONS[k])
else:
icon.setText("<b>" + k[:8] + "</b>")
icon.setProperty("style", "icon-armor")
self.unit_layout.addWidget(icon, i, 0)
self.unit_layout.addWidget(QLabel(str(v) + " x " + "<strong>" + k + "</strong>"), i, 1)
i = i + 1
manage_button = QPushButton("Manage")
manage_button.setProperty("style", "btn-success")
manage_button.setMaximumWidth(180)
manage_button.clicked.connect(self.onManage)
layout.addWidget(manage_button, i+1, 0)
self.setLayout(layout)
if len(unit_dict.items()) == 0:
self.unit_layout.addWidget(QLabel("/"), 0, 0)
self.setLayout(self.main_layout)
def onManage(self):
self.editionMenu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game)
self.editionMenu.show()
self.edition_menu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game)
self.edition_menu.show()
self.edition_menu.changed.connect(self.onEdition)
def onEdition(self):
self.buildLayout()

View File

@ -23,7 +23,6 @@ class QBaseInformation(QFrame):
scroll_content = QWidget()
task_box_layout = QGridLayout()
scroll_content.setLayout(task_box_layout)
row = 0
for g in self.cp.ground_objects:
if g.airbase_group:

View File

@ -1,12 +1,16 @@
import logging
from PySide2.QtGui import QCloseEvent
from PySide2.QtWidgets import QHBoxLayout, QWidget, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton
from PySide2 import QtCore
from PySide2.QtGui import Qt
from PySide2.QtWidgets import QHBoxLayout, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton, \
QComboBox, QSpinBox, QMessageBox
from dcs import Point
from game import Game
from game import Game, db
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import PRICES, unit_type_of
from game.db import PRICES, unit_type_of, PinpointStrike
from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@ -16,6 +20,8 @@ from theater import ControlPoint, TheaterGroundObject
class QGroundObjectMenu(QDialog):
changed = QtCore.Signal()
def __init__(self, parent, ground_object: TheaterGroundObject, buildings:[], cp: ControlPoint, game: Game):
super(QGroundObjectMenu, self).__init__(parent)
self.setMinimumWidth(350)
@ -29,6 +35,8 @@ class QGroundObjectMenu(QDialog):
self.buildingBox = QGroupBox("Buildings :")
self.intelLayout = QGridLayout()
self.buildingsLayout = QGridLayout()
self.sell_all_button = None
self.total_value = 0
self.init_ui()
def init_ui(self):
@ -43,9 +51,28 @@ class QGroundObjectMenu(QDialog):
self.mainLayout.addWidget(self.intelBox)
else:
self.mainLayout.addWidget(self.buildingBox)
self.actionLayout = QHBoxLayout()
self.sell_all_button = QPushButton("Disband (+" + str(self.total_value) + "M)")
self.sell_all_button.clicked.connect(self.sell_all)
self.sell_all_button.setProperty("style", "btn-danger")
self.buy_replace = QPushButton("Buy/Replace")
self.buy_replace.clicked.connect(self.buy_group)
self.buy_replace.setProperty("style", "btn-success")
if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace)
if self.cp.captured and self.ground_object.dcs_identifier == "AA":
self.mainLayout.addLayout(self.actionLayout)
self.setLayout(self.mainLayout)
def doLayout(self):
self.update_total_value()
self.intelBox = QGroupBox("Units :")
self.intelLayout = QGridLayout()
i = 0
@ -71,6 +98,9 @@ class QGroundObjectMenu(QDialog):
repair.clicked.connect(lambda u=u, g=g, p=price: self.repair_unit(g, u, p))
self.intelLayout.addWidget(repair, i, 1)
i = i + 1
stretch = QVBoxLayout()
stretch.addStretch()
self.intelLayout.addLayout(stretch, i, 0)
self.buildingBox = QGroupBox("Buildings :")
self.buildingsLayout = QGridLayout()
@ -86,14 +116,44 @@ class QGroundObjectMenu(QDialog):
def do_refresh_layout(self):
try:
for i in range(self.mainLayout.count()):
self.mainLayout.removeItem(self.mainLayout.itemAt(i))
item = self.mainLayout.itemAt(i)
if item is not None and item.widget() is not None:
item.widget().setParent(None)
self.sell_all_button.setParent(None)
self.buy_replace.setParent(None)
self.actionLayout.setParent(None)
self.doLayout()
if len(self.ground_object.groups) > 0:
if self.ground_object.dcs_identifier == "AA":
self.mainLayout.addWidget(self.intelBox)
else:
self.mainLayout.addWidget(self.buildingBox)
self.actionLayout = QHBoxLayout()
if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace)
if self.cp.captured and self.ground_object.dcs_identifier == "AA":
self.mainLayout.addLayout(self.actionLayout)
except Exception as e:
print(e)
self.update_total_value()
self.changed.emit()
def update_total_value(self):
total_value = 0
for group in self.ground_object.groups:
for u in group.units:
utype = unit_type_of(u)
if utype in PRICES:
total_value = total_value + PRICES[utype]
else:
total_value = total_value + 1
if self.sell_all_button is not None:
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
self.total_value = total_value
def repair_unit(self, group, unit, price):
if self.game.budget > price:
@ -112,6 +172,168 @@ class QGroundObjectMenu(QDialog):
logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type))
self.do_refresh_layout()
self.changed.emit()
def closeEvent(self, closeEvent: QCloseEvent):
GameUpdateSignal.get_instance().updateGame(self.game)
def sell_all(self):
self.update_total_value()
self.game.budget = self.game.budget + self.total_value
self.ground_object.groups = []
self.do_refresh_layout()
GameUpdateSignal.get_instance().updateBudget(self.game)
def buy_group(self):
self.subwindow = QBuyGroupForGroundObjectDialog(self, self.ground_object, self.cp, self.game, self.total_value)
self.subwindow.changed.connect(self.do_refresh_layout)
self.subwindow.show()
class QBuyGroupForGroundObjectDialog(QDialog):
changed = QtCore.Signal()
def __init__(self, parent, ground_object: TheaterGroundObject, cp: ControlPoint, game: Game, current_group_value: int):
super(QBuyGroupForGroundObjectDialog, self).__init__(parent)
self.setMinimumWidth(350)
self.ground_object = ground_object
self.cp = cp
self.game = game
self.current_group_value = current_group_value
self.setWindowTitle("Buy units @ " + self.ground_object.obj_name)
self.setWindowIcon(EVENT_ICONS["capture"])
self.buySamButton = QPushButton("Buy")
self.buyArmorButton = QPushButton("Buy")
self.buySamLayout = QGridLayout()
self.buyArmorLayout = QGridLayout()
self.amount = QSpinBox()
self.buyArmorCombo = QComboBox()
self.samCombo = QComboBox()
self.buySamBox = QGroupBox("Buy SAM site :")
self.buyArmorBox = QGroupBox("Buy defensive position :")
self.init_ui()
def init_ui(self):
faction = self.game.player_name
# Sams
possible_sams = get_faction_possible_sams_generator(faction)
for sam in possible_sams:
self.samCombo.addItem(sam.name + " [$" + str(sam.price) + "M]", userData=sam)
self.samCombo.currentIndexChanged.connect(self.samComboChanged)
self.buySamLayout.addWidget(QLabel("Site Type :"), 0, 0, Qt.AlignLeft)
self.buySamLayout.addWidget(self.samCombo, 0, 1, alignment=Qt.AlignRight)
self.buySamLayout.addWidget(self.buySamButton, 1, 1, alignment=Qt.AlignRight)
stretch = QVBoxLayout()
stretch.addStretch()
self.buySamLayout.addLayout(stretch, 2, 0)
self.buySamButton.clicked.connect(self.buySam)
# Armored units
armored_units = db.find_unittype(PinpointStrike, faction) # Todo : refactor this legacy nonsense
for unit in set(armored_units):
self.buyArmorCombo.addItem(db.unit_type_name_2(unit) + " [$" + str(db.PRICES[unit]) + "M]", userData=unit)
self.buyArmorCombo.currentIndexChanged.connect(self.armorComboChanged)
self.amount.setMinimum(2)
self.amount.setMaximum(8)
self.amount.setValue(2)
self.amount.valueChanged.connect(self.amountComboChanged)
self.buyArmorLayout.addWidget(QLabel("Unit type :"), 0, 0, Qt.AlignLeft)
self.buyArmorLayout.addWidget(self.buyArmorCombo, 0, 1, alignment=Qt.AlignRight)
self.buyArmorLayout.addWidget(QLabel("Group size :"), 1, 0, alignment=Qt.AlignLeft)
self.buyArmorLayout.addWidget(self.amount, 1, 1, alignment=Qt.AlignRight)
self.buyArmorLayout.addWidget(self.buyArmorButton, 2, 1, alignment=Qt.AlignRight)
stretch2 = QVBoxLayout()
stretch2.addStretch()
self.buyArmorLayout.addLayout(stretch2, 3, 0)
self.buyArmorButton.clicked.connect(self.buyArmor)
# Do layout
self.buySamBox.setLayout(self.buySamLayout)
self.buyArmorBox.setLayout(self.buyArmorLayout)
self.mainLayout = QHBoxLayout()
self.mainLayout.addWidget(self.buySamBox)
if self.ground_object.airbase_group:
self.mainLayout.addWidget(self.buyArmorBox)
self.setLayout(self.mainLayout)
try:
self.samComboChanged(0)
self.armorComboChanged(0)
except:
pass
def samComboChanged(self, index):
self.buySamButton.setText("Buy [$" + str(self.samCombo.itemData(index).price) + "M] [-$" + str(self.current_group_value) + "M]")
def armorComboChanged(self, index):
self.buyArmorButton.setText("Buy [$" + str(db.PRICES[self.buyArmorCombo.itemData(index)] * self.amount.value()) + "M][-$" + str(self.current_group_value) + "M]")
def amountComboChanged(self):
self.buyArmorButton.setText("Buy [$" + str(db.PRICES[self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())] * self.amount.value()) + "M][-$" + str(self.current_group_value) + "M]")
def buyArmor(self):
print("Buy Armor ")
utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
print(utype)
price = db.PRICES[utype] * self.amount.value() - self.current_group_value
if price > self.game.budget:
self.error_money()
self.close()
return
else:
self.game.budget -= price
# Generate Armor
group = generate_armor_group_of_type_and_size(self.game, self.ground_object, utype, int(self.amount.value()))
self.ground_object.groups = [group]
GameUpdateSignal.get_instance().updateBudget(self.game)
self.changed.emit()
self.close()
def buySam(self):
sam_generator = self.samCombo.itemData(self.samCombo.currentIndex())
price = sam_generator.price - self.current_group_value
if price > self.game.budget:
self.error_money()
return
else:
self.game.budget -= price
# Generate SAM
generator = sam_generator(self.game, self.ground_object)
generator.generate()
generated_group = generator.get_generated_group()
self.ground_object.groups = [generated_group]
GameUpdateSignal.get_instance().updateBudget(self.game)
self.changed.emit()
self.close()
def error_money(self):
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText("Not enough money to buy these units !")
msg.setWindowTitle("Not enough money")
msg.setStandardButtons(QMessageBox.Ok)
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
msg.exec_()
self.close()

View File

@ -17,7 +17,6 @@ class QMissionPlanning(QDialog):
self.game = game
self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.setMinimumSize(1000, 440)
self.setModal(True)
self.setWindowTitle("Mission Preparation")
self.setWindowIcon(EVENT_ICONS["strike"])
self.init_ui()

View File

@ -12,7 +12,7 @@ import qt_ui.uiconstants as CONST
from game import db, Game
from game.settings import Settings
from gen import namegen
from qt_ui.windows.newgame.QCampaignList import QCampaignList
from qt_ui.windows.newgame.QCampaignList import QCampaignList, CAMPAIGNS
from theater import start_generator, persiangulf, nevada, caucasus, ConflictTheater, normandy, thechannel
@ -42,6 +42,8 @@ class NewGameWizard(QtWidgets.QWizard):
redFaction = [c for c in db.FACTIONS][self.field("redFaction")]
selectedCampaign = self.field("selectedCampaign")
if selectedCampaign is None:
selectedCampaign = CAMPAIGNS[0]
conflictTheater = selectedCampaign[1]()
timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]]

View File

@ -2,3 +2,6 @@
Pyside2>=5.13.0
pyinstaller==3.6
pyproj==2.6.1.post1
Pillow~=7.2.0
tabulate~=0.8.7

View File

@ -2,41 +2,84 @@ local unitPayloads = {
["name"] = "F-16C_50",
["payloads"] = {
[1] = {
["name"] = "ANTISHIP",
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "LAU3_HE5",
["num"] = 6,
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
["num"] = 5,
},
[2] = {
["CLSID"] = "LAU3_HE5",
["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}",
["num"] = 7,
},
[3] = {
["CLSID"] = "LAU3_HE5",
["num"] = 4,
},
[4] = {
["CLSID"] = "LAU3_HE5",
["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}",
["num"] = 3,
},
[5] = {
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 2,
},
[6] = {
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[7] = {
[6] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 8,
},
[8] = {
[7] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 9,
},
[8] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}",
["num"] = 11,
},
},
["tasks"] = {
},
},
[2] = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}",
["num"] = 7,
},
[2] = {
["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}",
["num"] = 3,
},
[3] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 2,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 8,
},
[6] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}",
["num"] = 11,
},
[8] = {
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
["num"] = 5,
},
@ -44,7 +87,7 @@ local unitPayloads = {
["tasks"] = {
},
},
[2] = {
[3] = {
["name"] = "CAP",
["pylons"] = {
[1] = {
@ -87,53 +130,6 @@ local unitPayloads = {
["tasks"] = {
},
},
[3] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
["num"] = 5,
},
[2] = {
["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}",
["num"] = 7,
},
[3] = {
["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}",
["num"] = 3,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 2,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[6] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 8,
},
[7] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 9,
},
[8] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["num"] = 6,
},
[11] = {
["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}",
["num"] = 11,
},
},
["tasks"] = {
},
},
[4] = {
["name"] = "STRIKE",
["pylons"] = {
@ -142,11 +138,11 @@ local unitPayloads = {
["num"] = 7,
},
[2] = {
["CLSID"] = "{TER_9A_2L*MK-82}",
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 4,
},
[3] = {
["CLSID"] = "{TER_9A_2R*MK-82}",
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 6,
},
[4] = {
@ -173,7 +169,7 @@ local unitPayloads = {
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
["num"] = 5,
},
[11] = {
[10] = {
["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}",
["num"] = 11,
},
@ -185,19 +181,19 @@ local unitPayloads = {
["name"] = "SEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 6,
},
[2] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
["num"] = 7,
},
[3] = {
["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
[4] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
["num"] = 3,
},
[5] = {

View File

@ -2,26 +2,6 @@ local unitPayloads = {
["name"] = "P-47D-30",
["payloads"] = {
[1] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 11,
},
},
[2] = {
["name"] = "STRIKE",
["pylons"] = {
[1] = {
@ -41,14 +21,34 @@ local unitPayloads = {
[1] = 11,
},
},
[3] = {
["name"] = "ANTISHIP",
[2] = {
["name"] = "ANTISTRIKE",
["pylons"] = {
},
["tasks"] = {
[1] = 11,
},
},
[3] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 11,
},
},
[4] = {
["name"] = "CAP",
["pylons"] = {
@ -77,6 +77,25 @@ local unitPayloads = {
[1] = 11,
},
},
[6] = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
},
},
},
["tasks"] = {
},

View File

@ -0,0 +1,96 @@
local unitPayloads = {
["name"] = "P-47D-30bl1",
["payloads"] = {
[1] = {
["name"] = "CAP",
["pylons"] = {
},
["tasks"] = {
[1] = 11,
},
},
[2] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{AN_M57}",
["num"] = 1,
},
[2] = {
["CLSID"] = "{AN_M57}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN_M57}",
["num"] = 3,
},
},
["tasks"] = {
[1] = 11,
},
},
[3] = {
["name"] = "STRIKE",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 11,
},
},
[4] = {
["name"] = "SEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 11,
},
},
[5] = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
},
},
},
["tasks"] = {
},
["unitType"] = "P-47D-30bl1",
}
return unitPayloads

View File

@ -0,0 +1,88 @@
local unitPayloads = {
["name"] = "P-47D-40",
["payloads"] = {
[1] = {
["name"] = "CAP",
["pylons"] = {
},
["tasks"] = {
[1] = 11,
},
},
[2] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}",
["num"] = 4,
},
[2] = {
["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 11,
},
},
[3] = {
["name"] = "SEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}",
["num"] = 4,
},
[2] = {
["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 11,
},
},
[4] = {
["name"] = "STRIKE",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 11,
},
},
[5] = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}",
["num"] = 5,
},
[2] = {
["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}",
["num"] = 4,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
},
},
},
["tasks"] = {
},
["unitType"] = "P-47D-40",
}
return unitPayloads

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,317 @@
[
{
"name": "",
"callsign": "ICRR",
"beacon_type": 15,
"hertz": 108700000,
"channel": 24
},
{
"name": "",
"callsign": "ICRR",
"beacon_type": 14,
"hertz": 108700000,
"channel": 24
},
{
"name": "",
"callsign": "ICRS",
"beacon_type": 14,
"hertz": 108500000,
"channel": 22
},
{
"name": "",
"callsign": "ICRS",
"beacon_type": 15,
"hertz": 108500000,
"channel": 22
},
{
"name": "Indian Springs",
"callsign": "INS",
"beacon_type": 5,
"hertz": null,
"channel": 87
},
{
"name": "",
"callsign": "GLRI",
"beacon_type": 14,
"hertz": 109300000,
"channel": 30
},
{
"name": "",
"callsign": "GLRI",
"beacon_type": 15,
"hertz": 109300000,
"channel": 30
},
{
"name": "Groom Lake",
"callsign": "GRL",
"beacon_type": 5,
"hertz": null,
"channel": 18
},
{
"name": "",
"callsign": "I-RLE",
"beacon_type": 15,
"hertz": 111750000,
"channel": null
},
{
"name": "",
"callsign": "I-LAS",
"beacon_type": 15,
"hertz": 110300000,
"channel": 40
},
{
"name": "",
"callsign": "I-RLE",
"beacon_type": 14,
"hertz": 111750000,
"channel": null
},
{
"name": "",
"callsign": "I-LAS",
"beacon_type": 14,
"hertz": 110300000,
"channel": 40
},
{
"name": "Las Vegas",
"callsign": "LAS",
"beacon_type": 6,
"hertz": 116900000,
"channel": 116
},
{
"name": "",
"callsign": "IDIQ",
"beacon_type": 15,
"hertz": 109100000,
"channel": null
},
{
"name": "Nellis",
"callsign": "LSV",
"beacon_type": 5,
"hertz": null,
"channel": 12
},
{
"name": "",
"callsign": "IDIQ",
"beacon_type": 14,
"hertz": 109100000,
"channel": null
},
{
"name": "",
"callsign": "I-HWG",
"beacon_type": 14,
"hertz": 110700000,
"channel": null
},
{
"name": "",
"callsign": "I-HWG",
"beacon_type": 15,
"hertz": 110700000,
"channel": null
},
{
"name": "",
"callsign": "I-RVP",
"beacon_type": 14,
"hertz": 108300000,
"channel": null
},
{
"name": "",
"callsign": "I-UVV",
"beacon_type": 14,
"hertz": 111700000,
"channel": null
},
{
"name": "",
"callsign": "I-UVV",
"beacon_type": 15,
"hertz": 111700000,
"channel": null
},
{
"name": "",
"callsign": "I-RVP",
"beacon_type": 15,
"hertz": 108300000,
"channel": null
},
{
"name": "Silverbow",
"callsign": "TQQ",
"beacon_type": 6,
"hertz": 113000000,
"channel": 77
},
{
"name": "St George",
"callsign": "UTI",
"beacon_type": 4,
"hertz": 108600000,
"channel": 23
},
{
"name": "Grand Canyon",
"callsign": "GCN",
"beacon_type": 4,
"hertz": 113100000,
"channel": 78
},
{
"name": "Kingman",
"callsign": "IGM",
"beacon_type": 4,
"hertz": 108800000,
"channel": 25
},
{
"name": "Colorado City",
"callsign": "AZC",
"beacon_type": 10,
"hertz": 403000,
"channel": null
},
{
"name": "Meggi",
"callsign": "EC",
"beacon_type": 10,
"hertz": 217000,
"channel": null
},
{
"name": "Daggett",
"callsign": "DAG",
"beacon_type": 6,
"hertz": 113200000,
"channel": 79
},
{
"name": "Hector",
"callsign": "HEC",
"beacon_type": 6,
"hertz": 112700000,
"channel": 74
},
{
"name": "Needles",
"callsign": "EED",
"beacon_type": 6,
"hertz": 115200000,
"channel": 99
},
{
"name": "Milford",
"callsign": "MLF",
"beacon_type": 6,
"hertz": 112100000,
"channel": 58
},
{
"name": "GOFFS",
"callsign": "GFS",
"beacon_type": 6,
"hertz": 114400000,
"channel": 91
},
{
"name": "Tonopah",
"callsign": "TPH",
"beacon_type": 6,
"hertz": 117200000,
"channel": 119
},
{
"name": "Mina",
"callsign": "MVA",
"beacon_type": 6,
"hertz": 115100000,
"channel": 98
},
{
"name": "Wilson Creek",
"callsign": "ILC",
"beacon_type": 6,
"hertz": 116300000,
"channel": 110
},
{
"name": "Cedar City",
"callsign": "CDC",
"beacon_type": 6,
"hertz": 117300000,
"channel": 120
},
{
"name": "Bryce Canyon",
"callsign": "BCE",
"beacon_type": 6,
"hertz": 112800000,
"channel": 75
},
{
"name": "Mormon Mesa",
"callsign": "MMM",
"beacon_type": 6,
"hertz": 114300000,
"channel": 90
},
{
"name": "Beatty",
"callsign": "BTY",
"beacon_type": 6,
"hertz": 114700000,
"channel": 94
},
{
"name": "Bishop",
"callsign": "BIH",
"beacon_type": 6,
"hertz": 109600000,
"channel": 33
},
{
"name": "Coaldale",
"callsign": "OAL",
"beacon_type": 6,
"hertz": 117700000,
"channel": 124
},
{
"name": "Peach Springs",
"callsign": "PGS",
"beacon_type": 6,
"hertz": 112000000,
"channel": 57
},
{
"name": "Boulder City",
"callsign": "BLD",
"beacon_type": 6,
"hertz": 116700000,
"channel": 114
},
{
"name": "Mercury",
"callsign": "MCY",
"beacon_type": 10,
"hertz": 326000,
"channel": null
}
]

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,709 @@
[
{
"name": "ABUDHABI",
"callsign": "ADV",
"beacon_type": 2,
"hertz": 114250000,
"channel": null
},
{
"name": "AbuDhabiInt",
"callsign": "ADV",
"beacon_type": 3,
"hertz": 114250000,
"channel": 119
},
{
"name": "Abumusa",
"callsign": "ABM",
"beacon_type": 3,
"hertz": 285000,
"channel": 101
},
{
"name": "AlAinInt",
"callsign": "ALN",
"beacon_type": 4,
"hertz": 112600000,
"channel": 119
},
{
"name": "AlBateenInt",
"callsign": "ALB",
"beacon_type": 2,
"hertz": 114000000,
"channel": 119
},
{
"name": "BandarAbbas",
"callsign": "BND",
"beacon_type": 4,
"hertz": 117200000,
"channel": 119
},
{
"name": "BandarAbbas",
"callsign": "BND",
"beacon_type": 9,
"hertz": 250000,
"channel": null
},
{
"name": "",
"callsign": "IBND",
"beacon_type": 14,
"hertz": 333800000,
"channel": null
},
{
"name": "",
"callsign": "IBND",
"beacon_type": 15,
"hertz": 333800000,
"channel": null
},
{
"name": "BandarAbbas",
"callsign": "BND",
"beacon_type": 5,
"hertz": null,
"channel": 78
},
{
"name": "BandarEJask",
"callsign": "KHM",
"beacon_type": 4,
"hertz": 116300000,
"channel": null
},
{
"name": "BandarEJask",
"callsign": "JSK",
"beacon_type": 9,
"hertz": 349000000,
"channel": null
},
{
"name": "BandarLengeh",
"callsign": "LEN",
"beacon_type": 9,
"hertz": 408000,
"channel": null
},
{
"name": "BandarLengeh",
"callsign": "LEN",
"beacon_type": 4,
"hertz": 114800000,
"channel": 95
},
{
"name": "",
"callsign": "MMA",
"beacon_type": 15,
"hertz": 111100000,
"channel": 48
},
{
"name": "",
"callsign": "LMA",
"beacon_type": 15,
"hertz": 108700000,
"channel": 24
},
{
"name": "",
"callsign": "IMA",
"beacon_type": 15,
"hertz": 109100000,
"channel": 28
},
{
"name": "",
"callsign": "RMA",
"beacon_type": 15,
"hertz": 114900000,
"channel": 24
},
{
"name": "",
"callsign": "MMA",
"beacon_type": 14,
"hertz": 111100000,
"channel": 48
},
{
"name": "",
"callsign": "RMA",
"beacon_type": 14,
"hertz": 114900000,
"channel": 24
},
{
"name": "",
"callsign": "LMA",
"beacon_type": 14,
"hertz": 108700000,
"channel": 24
},
{
"name": "",
"callsign": "IMA",
"beacon_type": 14,
"hertz": 109100000,
"channel": 28
},
{
"name": "AlDhafra",
"callsign": "MA",
"beacon_type": 6,
"hertz": 114900000,
"channel": 96
},
{
"name": "",
"callsign": "IDBW",
"beacon_type": 14,
"hertz": 109500000,
"channel": null
},
{
"name": "",
"callsign": "IDBR",
"beacon_type": 14,
"hertz": 110100000,
"channel": null
},
{
"name": "",
"callsign": "IDBE",
"beacon_type": 14,
"hertz": 111300000,
"channel": null
},
{
"name": "",
"callsign": "IDBL",
"beacon_type": 14,
"hertz": 110900000,
"channel": null
},
{
"name": "",
"callsign": "IDBL",
"beacon_type": 15,
"hertz": 110900000,
"channel": null
},
{
"name": "",
"callsign": "IDBR",
"beacon_type": 15,
"hertz": 110100000,
"channel": null
},
{
"name": "",
"callsign": "IDBE",
"beacon_type": 15,
"hertz": 111300000,
"channel": null
},
{
"name": "",
"callsign": "IDBW",
"beacon_type": 15,
"hertz": 109500000,
"channel": null
},
{
"name": "",
"callsign": "IJEA",
"beacon_type": 14,
"hertz": 111750000,
"channel": null
},
{
"name": "",
"callsign": "IJWA",
"beacon_type": 15,
"hertz": 109750000,
"channel": null
},
{
"name": "",
"callsign": "IJEA",
"beacon_type": 15,
"hertz": 111750000,
"channel": null
},
{
"name": "",
"callsign": "IJWA",
"beacon_type": 14,
"hertz": 109750000,
"channel": null
},
{
"name": "Fujairah",
"callsign": "FJV",
"beacon_type": 4,
"hertz": 113800000,
"channel": 85
},
{
"name": "",
"callsign": "IFJR",
"beacon_type": 15,
"hertz": 111500000,
"channel": null
},
{
"name": "",
"callsign": "IFJR",
"beacon_type": 14,
"hertz": 111500000,
"channel": null
},
{
"name": "Havadarya",
"callsign": "HDR",
"beacon_type": 5,
"hertz": 111000000,
"channel": 47
},
{
"name": "",
"callsign": "IBHD",
"beacon_type": 14,
"hertz": 108900000,
"channel": null
},
{
"name": "",
"callsign": "IBHD",
"beacon_type": 15,
"hertz": 108900000,
"channel": null
},
{
"name": "Jiroft",
"callsign": "JIR",
"beacon_type": 10,
"hertz": 276000,
"channel": null
},
{
"name": "KERMAN",
"callsign": "KER",
"beacon_type": 5,
"hertz": 122500000,
"channel": 97
},
{
"name": "KERMAN",
"callsign": "KER",
"beacon_type": 4,
"hertz": 112000000,
"channel": 57
},
{
"name": "KERMAN",
"callsign": "KER",
"beacon_type": 3,
"hertz": 290000000,
"channel": null
},
{
"name": "",
"callsign": "IBKS",
"beacon_type": 14,
"hertz": 110300000,
"channel": null
},
{
"name": "",
"callsign": "IBKS",
"beacon_type": 15,
"hertz": 110300000,
"channel": null
},
{
"name": "KishIsland",
"callsign": "KIH",
"beacon_type": 9,
"hertz": 201000000,
"channel": null
},
{
"name": "KishIsland",
"callsign": "KIH",
"beacon_type": 5,
"hertz": null,
"channel": 112
},
{
"name": "LAR",
"callsign": "LAR",
"beacon_type": 4,
"hertz": 117900000,
"channel": null
},
{
"name": "LAR",
"callsign": "OISL",
"beacon_type": 9,
"hertz": 224000,
"channel": null
},
{
"name": "LavanIsland",
"callsign": "LVA",
"beacon_type": 4,
"hertz": 116850000,
"channel": 115
},
{
"name": "LavanIsland",
"callsign": "LVA",
"beacon_type": 9,
"hertz": 310000000,
"channel": 0
},
{
"name": "LiwaAirbase",
"callsign": "\u00c4\u00bc",
"beacon_type": 7,
"hertz": null,
"channel": 121
},
{
"name": "Minhad",
"callsign": "MIN",
"beacon_type": 5,
"hertz": 115200000,
"channel": 99
},
{
"name": "",
"callsign": "IMNW",
"beacon_type": 14,
"hertz": 110700000,
"channel": null
},
{
"name": "",
"callsign": "IMNW",
"beacon_type": 15,
"hertz": 110700000,
"channel": null
},
{
"name": "",
"callsign": "IMNR",
"beacon_type": 14,
"hertz": 110750000,
"channel": null
},
{
"name": "",
"callsign": "IMNR",
"beacon_type": 15,
"hertz": 110750000,
"channel": null
},
{
"name": "GheshmIsland",
"callsign": "KHM",
"beacon_type": 9,
"hertz": 233000,
"channel": null
},
{
"name": "GheshmIsland",
"callsign": "KHM",
"beacon_type": 4,
"hertz": 117100000,
"channel": null
},
{
"name": "RasAlKhaimah",
"callsign": "OMRK",
"beacon_type": 4,
"hertz": 113600000,
"channel": 83
},
{
"name": "SasAlNakheelAirport",
"callsign": "SAS",
"beacon_type": 10,
"hertz": 128925,
"channel": null
},
{
"name": "SasAlNakheel",
"callsign": "SAS",
"beacon_type": 4,
"hertz": 128925000,
"channel": 119
},
{
"name": "",
"callsign": "ISRE",
"beacon_type": 14,
"hertz": 108550000,
"channel": null
},
{
"name": "",
"callsign": "ISHW",
"beacon_type": 14,
"hertz": 111950000,
"channel": null
},
{
"name": "",
"callsign": "ISHW",
"beacon_type": 15,
"hertz": 111950000,
"channel": null
},
{
"name": "",
"callsign": "ISRE",
"beacon_type": 15,
"hertz": 108550000,
"channel": null
},
{
"name": "SHIRAZ",
"callsign": "SYZ",
"beacon_type": 4,
"hertz": 117800000,
"channel": 125
},
{
"name": "SHIRAZ",
"callsign": "SYZ1",
"beacon_type": 5,
"hertz": 114700000,
"channel": 94
},
{
"name": "SHIRAZ",
"callsign": "SR",
"beacon_type": 9,
"hertz": 205000,
"channel": null
},
{
"name": "",
"callsign": "ISYZ",
"beacon_type": 15,
"hertz": 109900000,
"channel": null
},
{
"name": "",
"callsign": "ISYZ",
"beacon_type": 14,
"hertz": 109900000,
"channel": null
},
{
"name": "SirriIsland",
"callsign": "SIR",
"beacon_type": 9,
"hertz": 300000,
"channel": null
},
{
"name": "SirriIsland",
"callsign": "SIR",
"beacon_type": 4,
"hertz": 113750000,
"channel": null
},
{
"name": "Kochak",
"callsign": "KCK",
"beacon_type": 5,
"hertz": 114200000,
"channel": 89
},
{
"name": "Kish",
"callsign": "KIS",
"beacon_type": 4,
"hertz": 117400000,
"channel": 121
},
{
"name": "DohaAirport",
"callsign": "DIA",
"beacon_type": 4,
"hertz": 112400000,
"channel": 71
},
{
"name": "HamadInternationalAirport",
"callsign": "DOH",
"beacon_type": 4,
"hertz": 114400000,
"channel": 91
},
{
"name": "DezfulAirport",
"callsign": "DZF",
"beacon_type": 9,
"hertz": 293000000,
"channel": null
},
{
"name": "AbadanIntAirport",
"callsign": "ABD",
"beacon_type": 4,
"hertz": 115100000,
"channel": 98
},
{
"name": "AhvazIntAirport",
"callsign": "AWZ",
"beacon_type": 4,
"hertz": 114000000,
"channel": 87
},
{
"name": "AghajariAirport",
"callsign": "AJR",
"beacon_type": 4,
"hertz": 114900000,
"channel": 96
},
{
"name": "BirjandIntAirport",
"callsign": "BJD",
"beacon_type": 4,
"hertz": 113500000,
"channel": 82
},
{
"name": "BushehrIntAirport",
"callsign": "BUZ",
"beacon_type": 4,
"hertz": 117450000,
"channel": 121
},
{
"name": "KonarakAirport",
"callsign": "CBH",
"beacon_type": 4,
"hertz": 115600000,
"channel": 103
},
{
"name": "IsfahanIntAirport",
"callsign": "ISN",
"beacon_type": 4,
"hertz": 113200000,
"channel": 79
},
{
"name": "KhoramabadAirport",
"callsign": "KRD",
"beacon_type": 4,
"hertz": 113750000,
"channel": 84
},
{
"name": "PersianGulfIntAirport",
"callsign": "PRG",
"beacon_type": 4,
"hertz": 112100000,
"channel": 58
},
{
"name": "YasoujAirport",
"callsign": "YSJ",
"beacon_type": 4,
"hertz": 116550000,
"channel": 112
},
{
"name": "BamAirport",
"callsign": "BAM",
"beacon_type": 4,
"hertz": 114900000,
"channel": 96
},
{
"name": "MahshahrAirport",
"callsign": "MAH",
"beacon_type": 4,
"hertz": 115800000,
"channel": 105
},
{
"name": "IranShahrAirport",
"callsign": "ISR",
"beacon_type": 4,
"hertz": 117000000,
"channel": 117
},
{
"name": "LamerdAirport",
"callsign": "LAM",
"beacon_type": 4,
"hertz": 117000000,
"channel": 117
},
{
"name": "SirjanAirport",
"callsign": "SRJ",
"beacon_type": 4,
"hertz": 114600000,
"channel": 93
},
{
"name": "YazdIntAirport",
"callsign": "YZD",
"beacon_type": 4,
"hertz": 117700000,
"channel": 124
},
{
"name": "ZabolAirport",
"callsign": "ZAL",
"beacon_type": 4,
"hertz": 113100000,
"channel": 78
},
{
"name": "ZahedanIntAirport",
"callsign": "ZDN",
"beacon_type": 4,
"hertz": 116000000,
"channel": 107
},
{
"name": "RafsanjanAirport",
"callsign": "RAF",
"beacon_type": 4,
"hertz": 112300000,
"channel": 70
},
{
"name": "SaravanAirport",
"callsign": "SRN",
"beacon_type": 4,
"hertz": 114100000,
"channel": 88
},
{
"name": "BuHasa",
"callsign": "BH",
"beacon_type": 3,
"hertz": 309000000,
"channel": null
}
]

View File

@ -0,0 +1,408 @@
[
{
"name": "Deir ez-Zor",
"callsign": "DRZ",
"beacon_type": 10,
"hertz": 295000,
"channel": null
},
{
"name": "GAZIANTEP",
"callsign": "GAZ",
"beacon_type": 10,
"hertz": 432000,
"channel": null
},
{
"name": "BANIAS",
"callsign": "BAN",
"beacon_type": 10,
"hertz": 304000,
"channel": null
},
{
"name": "ALEPPO",
"callsign": "ALE",
"beacon_type": 10,
"hertz": 396000,
"channel": null
},
{
"name": "KAHRAMANMARAS",
"callsign": "KHM",
"beacon_type": 10,
"hertz": 374000,
"channel": null
},
{
"name": "MEZZEH",
"callsign": "MEZ",
"beacon_type": 10,
"hertz": 358000,
"channel": null
},
{
"name": "KLEYATE",
"callsign": "RA",
"beacon_type": 10,
"hertz": 450000,
"channel": null
},
{
"name": "KARIATAIN",
"callsign": "KTN",
"beacon_type": 10,
"hertz": 372500,
"channel": null
},
{
"name": "ALEPPO",
"callsign": "MER",
"beacon_type": 10,
"hertz": 365000,
"channel": null
},
{
"name": "TURAIF",
"callsign": "TRF",
"beacon_type": 4,
"hertz": 116100000,
"channel": null
},
{
"name": "Deir ez-Zor",
"callsign": "DRZ",
"beacon_type": 4,
"hertz": 117000000,
"channel": null
},
{
"name": "BAYSUR",
"callsign": "BAR",
"beacon_type": 4,
"hertz": 113900000,
"channel": null
},
{
"name": "ALEPPO",
"callsign": "ALE",
"beacon_type": 4,
"hertz": 114500000,
"channel": null
},
{
"name": "MARKA",
"callsign": "AMN",
"beacon_type": 4,
"hertz": 116300000,
"channel": null
},
{
"name": "GAZIANTEP",
"callsign": "GAZ",
"beacon_type": 4,
"hertz": 116700000,
"channel": null
},
{
"name": "ROSH-PINA",
"callsign": "ROP",
"beacon_type": 4,
"hertz": 115300000,
"channel": null
},
{
"name": "TANF",
"callsign": "TAN",
"beacon_type": 4,
"hertz": 114000000,
"channel": null
},
{
"name": "NATANIA",
"callsign": "NAT",
"beacon_type": 4,
"hertz": 112400000,
"channel": null
},
{
"name": "KAHRAMANMARAS",
"callsign": "KHM",
"beacon_type": 4,
"hertz": 113900000,
"channel": null
},
{
"name": "KARIATAIN",
"callsign": "KTN",
"beacon_type": 4,
"hertz": 117700000,
"channel": null
},
{
"name": "",
"callsign": "IADA",
"beacon_type": 14,
"hertz": 108700000,
"channel": null
},
{
"name": "",
"callsign": "IADA",
"beacon_type": 15,
"hertz": 108700000,
"channel": null
},
{
"name": "ADANA",
"callsign": "ADN",
"beacon_type": 11,
"hertz": 395000000,
"channel": null
},
{
"name": "ADANA",
"callsign": "ADA",
"beacon_type": 4,
"hertz": 112700000,
"channel": null
},
{
"name": "KALDE",
"callsign": "KAD",
"beacon_type": 4,
"hertz": 112600000,
"channel": null
},
{
"name": "",
"callsign": "IBB",
"beacon_type": 15,
"hertz": 110100000,
"channel": null
},
{
"name": "",
"callsign": "IKK",
"beacon_type": 14,
"hertz": 110700000,
"channel": null
},
{
"name": "",
"callsign": "BIL",
"beacon_type": 14,
"hertz": 109500000,
"channel": null
},
{
"name": "",
"callsign": "IBB",
"beacon_type": 14,
"hertz": 110100000,
"channel": null
},
{
"name": "",
"callsign": "BIL",
"beacon_type": 15,
"hertz": 109500000,
"channel": null
},
{
"name": "",
"callsign": "IKK",
"beacon_type": 15,
"hertz": 110700000,
"channel": null
},
{
"name": "BEIRUT",
"callsign": "BOD",
"beacon_type": 11,
"hertz": 351000000,
"channel": null
},
{
"name": "",
"callsign": "IDA",
"beacon_type": 15,
"hertz": 109900000,
"channel": null
},
{
"name": "",
"callsign": "IDA",
"beacon_type": 14,
"hertz": 109900000,
"channel": null
},
{
"name": "Damascus",
"callsign": "DAM",
"beacon_type": 4,
"hertz": 116000000,
"channel": null
},
{
"name": "",
"callsign": "DAML",
"beacon_type": 14,
"hertz": 111100000,
"channel": null
},
{
"name": "DAMASCUS",
"callsign": "DAL",
"beacon_type": 11,
"hertz": 342000000,
"channel": null
},
{
"name": "ABYAD",
"callsign": "ABD",
"beacon_type": 10,
"hertz": 264000,
"channel": null
},
{
"name": "",
"callsign": "DAML",
"beacon_type": 15,
"hertz": 111100000,
"channel": null
},
{
"name": "HATAY",
"callsign": "HTY",
"beacon_type": 4,
"hertz": 112050000,
"channel": null
},
{
"name": "",
"callsign": "IHAT",
"beacon_type": 14,
"hertz": 108900000,
"channel": null
},
{
"name": "",
"callsign": "IHAT",
"beacon_type": 15,
"hertz": 108900000,
"channel": null
},
{
"name": "HATAY",
"callsign": "HTY",
"beacon_type": 10,
"hertz": 336000,
"channel": null
},
{
"name": "",
"callsign": "IHTY",
"beacon_type": 15,
"hertz": 108150000,
"channel": null
},
{
"name": "",
"callsign": "IHTY",
"beacon_type": 14,
"hertz": 108150000,
"channel": null
},
{
"name": "INCIRLIC",
"callsign": "DAN",
"beacon_type": 6,
"hertz": 108400000,
"channel": 21
},
{
"name": "",
"callsign": "IDAN",
"beacon_type": 14,
"hertz": 109300000,
"channel": null
},
{
"name": "",
"callsign": "IDAN",
"beacon_type": 15,
"hertz": 109300000,
"channel": null
},
{
"name": "",
"callsign": "DANM",
"beacon_type": 15,
"hertz": 111700000,
"channel": null
},
{
"name": "",
"callsign": "DANM",
"beacon_type": 14,
"hertz": 111700000,
"channel": null
},
{
"name": "",
"callsign": "IBA",
"beacon_type": 15,
"hertz": 109100000,
"channel": null
},
{
"name": "",
"callsign": "IBA",
"beacon_type": 14,
"hertz": 109100000,
"channel": null
},
{
"name": "LATAKIA",
"callsign": "LTK",
"beacon_type": 4,
"hertz": 114800000,
"channel": null
},
{
"name": "LATAKIA",
"callsign": "LTK",
"beacon_type": 11,
"hertz": 414000000,
"channel": null
},
{
"name": "PALMYRA",
"callsign": "PLR",
"beacon_type": 10,
"hertz": 363000,
"channel": null
},
{
"name": "PALMYRA",
"callsign": "PAL",
"beacon_type": 10,
"hertz": 337000,
"channel": null
},
{
"name": "RAMATDAVID",
"callsign": "RMD",
"beacon_type": 10,
"hertz": 368000,
"channel": null
},
{
"name": "Cheka",
"callsign": "CAK",
"beacon_type": 4,
"hertz": 116200000,
"channel": null
}
]

Binary file not shown.

38
resources/fonts/OFL.txt Normal file
View File

@ -0,0 +1,38 @@
—————————————————————————————-
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
—————————————————————————————-
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
“Font Software” refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
“Reserved Font Name” refers to any names specified as such after the copyright statement(s).
“Original Version” refers to the collection of Font Software components as distributed by the Copyright Holder(s).
“Modified Version” refers to any derivative made by adding to, deleting, or substituting—in part or in whole—any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
“Author” refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -198,6 +198,15 @@ QLabel[style="icon-plane"]{
color:white;
}
QLabel[style="icon-armor"]{
background-color:#48719D;
min-height:24px;
max-width: 64px;
border: 1px solid black;
text-align:center;
color:white;
}
QLabel[style="BARCAP"]{
border: 1px solid black;
background-color: #445299;

View File

@ -106,6 +106,15 @@ QLabel[style="icon-plane"]{
color:white;
}
QLabel[style="icon-armor"]{
background-color:#48719D;
min-height:24px;
max-width: 64px;
border: 1px solid black;
text-align:center;
color:white;
}
QLabel[style="bordered"]{
border: 1px solid black;
}

View File

@ -1,6 +1,6 @@
import os
import sys
from pydcs import dcs
import dcs
from game import db
from gen.aircraft import AircraftConflictGenerator
@ -30,6 +30,6 @@ for t, uts in db.UNIT_BY_TASK.items():
altitude=10000
)
g.task = t.name
airgen._setup_group(g, t, 0)
airgen._setup_group(g, t, 0, {})
mis.save("loadout_test.miz")

View File

@ -0,0 +1,183 @@
"""Generates resources/dcs/beacons.json from the DCS installation.
DCS has a beacons.lua file for each terrain mod that includes information about
the radio beacons present on the map:
beacons = {
{
display_name = _('INCIRLIC');
beaconId = 'airfield16_0';
type = BEACON_TYPE_VORTAC;
callsign = 'DAN';
frequency = 108400000.000000;
channel = 21;
position = { 222639.437500, 73.699811, -33216.257813 };
direction = 0.000000;
positionGeo = { latitude = 37.015611, longitude = 35.448194 };
sceneObjects = {'t:124814096'};
};
...
}
"""
import argparse
from contextlib import contextmanager
import dataclasses
import gettext
import os
from pathlib import Path
import textwrap
from typing import Dict, Iterable, Union
import lupa
import game # Needed to resolve cyclic import, for some reason.
from gen.beacons import Beacon, BeaconType, BEACONS_RESOURCE_PATH
THIS_DIR = Path(__file__).parent.resolve()
SRC_DIR = THIS_DIR.parents[1]
EXPORT_DIR = SRC_DIR / BEACONS_RESOURCE_PATH
@contextmanager
def cd(path: Path):
cwd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(cwd)
def convert_lua_frequency(raw: Union[float, int]) -> int:
if isinstance(raw, float):
if not raw.is_integer():
# The values are in hertz, and everything should be a whole number.
raise ValueError(f"Unexpected non-integer frequency: {raw}")
return int(raw)
else:
return raw
def beacons_from_terrain(dcs_path: Path, path: Path) -> Iterable[Beacon]:
# TODO: Fix case-sensitive issues.
# The beacons.lua file differs by case in some terrains. Will need to be
# fixed if the tool is to be run on Linux, but presumably the server
# wouldn't be able to find these anyway.
beacons_lua = path / "beacons.lua"
with cd(dcs_path):
lua = lupa.LuaRuntime()
lua.execute(textwrap.dedent("""\
function module(name)
end
"""))
bind_gettext = lua.eval(textwrap.dedent("""\
function(py_gettext)
package.preload["i_18n"] = function()
return {
translate = py_gettext
}
end
end
"""))
translator = gettext.translation(
"messages", path / "l10n", languages=["en"])
def translate(message_name: str) -> str:
if not message_name:
return message_name
return translator.gettext(message_name)
bind_gettext(translate)
src = beacons_lua.read_text()
lua.execute(src)
beacon_types_map: Dict[int, BeaconType] = {}
for beacon_type in BeaconType:
beacon_value = lua.eval(beacon_type.name)
beacon_types_map[beacon_value] = beacon_type
beacons = lua.eval("beacons")
for beacon in beacons.values():
beacon_type_lua = beacon["type"]
if beacon_type_lua not in beacon_types_map:
raise KeyError(
f"Unknown beacon type {beacon_type_lua}. Check that all "
f"beacon types in {beacon_types_path} are present in "
f"{BeaconType.__class__.__name__}"
)
beacon_type = beacon_types_map[beacon_type_lua]
yield Beacon(
beacon["display_name"],
beacon["callsign"],
beacon_type,
convert_lua_frequency(beacon["frequency"]),
getattr(beacon, "channel", None)
)
class Importer:
"""Imports beacon definitions from each available terrain mod.
Only beacons for maps owned by the user can be imported. Other maps that
have been previously imported will not be disturbed.
"""
def __init__(self, dcs_path: Path, export_dir: Path) -> None:
self.dcs_path = dcs_path
self.export_dir = export_dir
def run(self) -> None:
"""Exports the beacons for each available terrain mod."""
terrains_path = self.dcs_path / "Mods" / "terrains"
self.export_dir.mkdir(parents=True, exist_ok=True)
for terrain in terrains_path.iterdir():
beacons = beacons_from_terrain(self.dcs_path, terrain)
self.export_beacons(terrain.name, beacons)
def export_beacons(self, terrain: str, beacons: Iterable[Beacon]) -> None:
terrain_py_path = self.export_dir / f"{terrain.lower()}.json"
import json
terrain_py_path.write_text(json.dumps([
dataclasses.asdict(b) for b in beacons
], indent=True))
def parse_args() -> argparse.Namespace:
"""Parses and returns command line arguments."""
parser = argparse.ArgumentParser()
def resolved_path(val: str) -> Path:
"""Returns the given string as a fully resolved Path."""
return Path(val).resolve()
parser.add_argument(
"--export-to",
type=resolved_path,
default=EXPORT_DIR,
help="Output directory for generated JSON files.")
parser.add_argument(
"dcs_path",
metavar="DCS_PATH",
type=resolved_path,
help="Path to DCS installation."
)
return parser.parse_args()
def main() -> None:
"""Program entry point."""
args = parse_args()
Importer(args.dcs_path, args.export_to).run()
if __name__ == "__main__":
main()

View File

@ -1,6 +1,6 @@
import typing
from pydcs import dcs
import dcs
from dcs.mapping import Point
from .controlpoint import ControlPoint

View File

@ -27,7 +27,6 @@ class ControlPoint:
full_name = None # type: str
base = None # type: theater.base.Base
at = None # type: db.StartPosition
icls = 1
allow_sea_units = True
connected_points = None # type: typing.List[ControlPoint]
@ -38,7 +37,6 @@ class ControlPoint:
frontline_offset = 0.0
cptype: ControlPointType = None
ICLS_counter = 1
alt = 0
def __init__(self, id: int, name: str, position: Point, at, radials: typing.Collection[int], size: int, importance: float,
@ -63,10 +61,6 @@ class ControlPoint:
self.base = theater.base.Base()
self.cptype = cptype
self.stances = {}
self.tacanY = False
self.tacanN = None
self.tacanI = "TAC"
self.icls = 0
self.airport = None
@classmethod
@ -81,11 +75,6 @@ class ControlPoint:
import theater.conflicttheater
cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
cp.tacanY = False
cp.tacanN = random.randint(26, 49)
cp.tacanI = random.choice(["STE", "CVN", "CVH", "CCV", "ACC", "ARC", "GER", "ABR", "LIN", "TRU"])
ControlPoint.ICLS_counter = ControlPoint.ICLS_counter + 1
cp.icls = ControlPoint.ICLS_counter
return cp
@classmethod
@ -93,9 +82,6 @@ class ControlPoint:
import theater.conflicttheater
cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
cp.tacanY = False
cp.tacanN = random.randint(1,25)
cp.tacanI = random.choice(["LHD", "LHA", "LHB", "LHC", "LHD", "LDS"])
return cp
@property

View File

@ -2,7 +2,7 @@ import json
import os
from shutil import copyfile
from pydcs import dcs
import dcs
from userdata import persistency