mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
commit
7eea328706
4
.gitignore
vendored
4
.gitignore
vendored
@ -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
19
.vscode/launch.json
vendored
Normal 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
4
.vscode/settings.json
vendored
Normal 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
35
.vscode/tasks.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
20
changelog.md
20
changelog.md
@ -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
|
||||
|
||||
|
||||
@ -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']
|
||||
|
||||
|
||||
27
game/db.py
27
game/db.py
@ -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,
|
||||
|
||||
@ -20,7 +20,7 @@ China_2010 = {
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
A_50,
|
||||
KJ_2000,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_28N,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -9,6 +9,7 @@ from .environmentgen import *
|
||||
from .groundobjectsgen import *
|
||||
from .briefinggen import *
|
||||
from .forcedoptionsgen import *
|
||||
from .kneeboard import *
|
||||
|
||||
from . import naming
|
||||
|
||||
|
||||
587
gen/aircraft.py
587
gen/aircraft.py
@ -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,9 +750,10 @@ 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
|
||||
if cp.airport is not None:
|
||||
logging.info("CLEARING SLOTS @ " + cp.airport.name)
|
||||
logging.info("===============")
|
||||
if cp.airport is not None:
|
||||
@ -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
1558
gen/airfields.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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))
|
||||
|
||||
self.air_support.awacs.append(AwacsInfo(
|
||||
callsign_for_support_unit(awacs_flight), freq))
|
||||
except:
|
||||
print("No AWACS for faction")
|
||||
|
||||
|
||||
25
gen/armor.py
25
gen/armor.py
@ -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
74
gen/beacons.py
Normal 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)
|
||||
@ -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"
|
||||
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 planner.flights:
|
||||
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"
|
||||
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
34
gen/callsigns.py
Normal 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)}"
|
||||
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import typing
|
||||
import pdb
|
||||
from pydcs import dcs
|
||||
import dcs
|
||||
|
||||
from random import randint
|
||||
from dcs import Mission
|
||||
|
||||
@ -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)
|
||||
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()
|
||||
return None
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
ingress_point.targets.append(location)
|
||||
ingress_point.targetGroup = location
|
||||
flight.points.append(point)
|
||||
else:
|
||||
point = FlightWaypoint(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
|
||||
flight.points.append(point)
|
||||
ingress_point.targets.append(location)
|
||||
ingress_point.targetGroup = location
|
||||
else:
|
||||
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 = "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
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
|
||||
|
||||
@ -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
290
gen/kneeboard.py
Normal 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
226
gen/radios.py
Normal 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)
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
83
gen/tacan.py
Normal 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
6
gen/units.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Unit conversions."""
|
||||
|
||||
|
||||
def meters_to_feet(meters: float) -> float:
|
||||
"""Convers meters to feet."""
|
||||
return meters * 3.28084
|
||||
2
pydcs
2
pydcs
@ -1 +1 @@
|
||||
Subproject commit dcc3d846316af2925c93ae09840c3ab4a1150e59
|
||||
Subproject commit f46781b854102a9f06948c8fb81a40331b78459e
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -102,3 +103,6 @@ class QTopPanel(QFrame):
|
||||
def proceed(self):
|
||||
self.subwindow = QMissionPlanning(self.game)
|
||||
self.subwindow.show()
|
||||
|
||||
def budget_update(self, game:Game):
|
||||
self.budgetBox.setGame(game)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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/>"\
|
||||
|
||||
@ -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()
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
@ -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()
|
||||
|
||||
@ -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")]]
|
||||
|
||||
@ -2,3 +2,6 @@
|
||||
Pyside2>=5.13.0
|
||||
pyinstaller==3.6
|
||||
pyproj==2.6.1.post1
|
||||
|
||||
Pillow~=7.2.0
|
||||
tabulate~=0.8.7
|
||||
@ -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] = {
|
||||
|
||||
@ -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"] = {
|
||||
},
|
||||
|
||||
96
resources/customized_payloads/P-47D-30bl1.lua
Normal file
96
resources/customized_payloads/P-47D-30bl1.lua
Normal 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
|
||||
88
resources/customized_payloads/P-47D-40.lua
Normal file
88
resources/customized_payloads/P-47D-40.lua
Normal 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
|
||||
1157
resources/dcs/beacons/caucasus.json
Normal file
1157
resources/dcs/beacons/caucasus.json
Normal file
File diff suppressed because it is too large
Load Diff
317
resources/dcs/beacons/nevada.json
Normal file
317
resources/dcs/beacons/nevada.json
Normal 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
|
||||
}
|
||||
]
|
||||
1
resources/dcs/beacons/normandy.json
Normal file
1
resources/dcs/beacons/normandy.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
709
resources/dcs/beacons/persiangulf.json
Normal file
709
resources/dcs/beacons/persiangulf.json
Normal 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
|
||||
}
|
||||
]
|
||||
408
resources/dcs/beacons/syria.json
Normal file
408
resources/dcs/beacons/syria.json
Normal 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
|
||||
}
|
||||
]
|
||||
BIN
resources/fonts/Inconsolata.otf
Normal file
BIN
resources/fonts/Inconsolata.otf
Normal file
Binary file not shown.
38
resources/fonts/OFL.txt
Normal file
38
resources/fonts/OFL.txt
Normal 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.
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
183
resources/tools/import_beacons.py
Normal file
183
resources/tools/import_beacons.py
Normal 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()
|
||||
@ -1,6 +1,6 @@
|
||||
import typing
|
||||
|
||||
from pydcs import dcs
|
||||
import dcs
|
||||
from dcs.mapping import Point
|
||||
|
||||
from .controlpoint import ControlPoint
|
||||
|
||||
@ -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
|
||||
|
||||
@ -2,7 +2,7 @@ import json
|
||||
import os
|
||||
from shutil import copyfile
|
||||
|
||||
from pydcs import dcs
|
||||
import dcs
|
||||
|
||||
from userdata import persistency
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user