Merge branch 'develop' into faction_refactor

# Conflicts:
#	game/factions/bluefor_coldwar.py
#	game/factions/bluefor_coldwar_a4.py
#	game/factions/bluefor_coldwar_mods.py
#	game/factions/bluefor_modern.py
This commit is contained in:
Khopa 2020-10-24 16:57:28 +02:00
commit f962fd55bc
36 changed files with 571 additions and 335 deletions

View File

@ -30,6 +30,9 @@ jobs:
- name: Build binaries - name: Build binaries
run: | run: |
./venv/scripts/activate ./venv/scripts/activate
mypy game
mypy gen
mypy theater
$env:PYTHONPATH=".;./pydcs" $env:PYTHONPATH=".;./pydcs"
pyinstaller pyinstaller.spec pyinstaller pyinstaller.spec

View File

@ -32,6 +32,9 @@ jobs:
- name: Build binaries - name: Build binaries
run: | run: |
./venv/scripts/activate ./venv/scripts/activate
mypy game
mypy gen
mypy theater
$env:PYTHONPATH=".;./pydcs" $env:PYTHONPATH=".;./pydcs"
pyinstaller pyinstaller.spec pyinstaller pyinstaller.spec

View File

@ -43,6 +43,7 @@ from dcs.planes import (
FA_18C_hornet, FA_18C_hornet,
FW_190A8, FW_190A8,
FW_190D9, FW_190D9,
F_117A,
F_14B, F_14B,
F_15C, F_15C,
F_15E, F_15E,
@ -97,6 +98,9 @@ from dcs.planes import (
Su_34, Su_34,
Tornado_GR4, Tornado_GR4,
Tornado_IDS, Tornado_IDS,
Tu_160,
Tu_22M3,
Tu_95MS,
WingLoong_I, WingLoong_I,
Yak_40, Yak_40,
plane_map, plane_map,
@ -304,6 +308,10 @@ PRICES = {
# Bombers # Bombers
B_52H: 35, B_52H: 35,
B_1B: 50, B_1B: 50,
F_117A: 100,
Tu_160: 50,
Tu_22M3: 40,
Tu_95MS: 35,
# special # special
IL_76MD: 30, IL_76MD: 30,
@ -584,49 +592,53 @@ UNIT_BY_TASK = {
SA342Mistral SA342Mistral
], ],
CAS: [ CAS: [
F_15E, AH_1W,
F_86F_Sabre, AH_64A,
MiG_15bis, AH_64D,
L_39ZA,
AV8BNA,
AJS37, AJS37,
AV8BNA,
A_10A, A_10A,
A_10C, A_10C,
A_10C_2, A_10C_2,
Su_17M4,
Su_25,
Su_25T,
Su_34,
Ka_50,
SA342M,
SA342L,
SA342Minigun,
Su_24M,
Su_24MR,
AH_64A,
AH_64D,
OH_58D,
B_52H,
B_1B,
Tornado_IDS,
Tornado_GR4,
UH_1H,
Mi_8MT,
Mi_28N,
Mi_24V,
MiG_27K,
A_20G, A_20G,
B_17G,
B_1B,
B_52H,
F_117A,
F_15E,
F_86F_Sabre,
Ju_88A4,
Ka_50,
L_39ZA,
MB_339PAN,
MQ_9_Reaper,
MiG_15bis,
MiG_27K,
Mi_24V,
Mi_28N,
Mi_8MT,
OH_58D,
P_47D_30, P_47D_30,
P_47D_30bl1, P_47D_30bl1,
P_47D_40, P_47D_40,
Ju_88A4,
B_17G,
MB_339PAN,
Rafale_A_S,
WingLoong_I,
MQ_9_Reaper,
RQ_1A_Predator, RQ_1A_Predator,
AH_1W Rafale_A_S,
SA342L,
SA342M,
SA342Minigun,
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_34,
Tornado_GR4,
Tornado_IDS,
Tu_160,
Tu_22M3,
Tu_95MS,
UH_1H,
WingLoong_I,
], ],
Transport: [ Transport: [
IL_76MD, IL_76MD,
@ -942,6 +954,23 @@ COMMON_OVERRIDE = {
PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
B_1B: {
CAS: "GBU-38*16, CBU-97*20",
PinpointStrike: "GBU-31*8, GBU-38*32",
GroundAttack: "GBU-31*8, GBU-38*32",
},
B_52H: {
PinpointStrike: "AGM-86C*20",
GroundAttack: "Mk 82*51",
},
F_117A: {
PinpointStrike: "GBU-10*2",
},
F_15E: {
CAS: "AIM-120B*2,AIM-9M*2,FUEL,GBU-12*4,GBU-38*4,AGM-65D*2",
GroundAttack: "AIM-120B*2,AIM-9M*2,FUEL*3,CBU-97*12",
PinpointStrike: "AIM-120B*2,AIM-9M*2,FUEL,GBU-31*4,AGM-154C*2",
},
FA_18C_hornet: { FA_18C_hornet: {
CAP: "CAP HEAVY", CAP: "CAP HEAVY",
Intercept: "CAP HEAVY", Intercept: "CAP HEAVY",
@ -962,6 +991,15 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
GroundAttack: "STRIKE", GroundAttack: "STRIKE",
Escort: "CAP HEAVY", Escort: "CAP HEAVY",
}, },
Tu_160: {
PinpointStrike: "Kh-65*12",
},
Tu_22M3: {
GroundAttack: "FAB-500*33, FAB-250*36",
},
Tu_95MS: {
PinpointStrike: "Kh-65*6",
},
A_10A: COMMON_OVERRIDE, A_10A: COMMON_OVERRIDE,
A_10C: COMMON_OVERRIDE, A_10C: COMMON_OVERRIDE,
A_10C_2: COMMON_OVERRIDE, A_10C_2: COMMON_OVERRIDE,
@ -970,7 +1008,6 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
F_5E_3: COMMON_OVERRIDE, F_5E_3: COMMON_OVERRIDE,
F_14B: COMMON_OVERRIDE, F_14B: COMMON_OVERRIDE,
F_15C: COMMON_OVERRIDE, F_15C: COMMON_OVERRIDE,
F_15E: COMMON_OVERRIDE,
F_16C_50: COMMON_OVERRIDE, F_16C_50: COMMON_OVERRIDE,
JF_17: COMMON_OVERRIDE, JF_17: COMMON_OVERRIDE,
M_2000C: COMMON_OVERRIDE, M_2000C: COMMON_OVERRIDE,

View File

@ -1,4 +1,12 @@
from dcs.planes import An_26B, An_30M, IL_76MD, IL_78M, MiG_15bis, Yak_40 from dcs.planes import (
An_26B,
An_30M,
IL_76MD,
IL_78M,
MiG_15bis,
Tu_95MS,
Yak_40,
)
from dcs.ships import ( from dcs.ships import (
Bulk_cargo_ship_Yakushev, Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov, CV_1143_5_Admiral_Kuznetsov,
@ -19,6 +27,8 @@ Russia_1955 = {
An_30M, An_30M,
Yak_40, Yak_40,
Tu_95MS,
AirDefence.AAA_ZU_23_Closed, AirDefence.AAA_ZU_23_Closed,
AirDefence.AAA_ZU_23_on_Ural_375, AirDefence.AAA_ZU_23_on_Ural_375,
Armor.ARV_BRDM_2, Armor.ARV_BRDM_2,

View File

@ -8,6 +8,7 @@ from dcs.planes import (
MiG_15bis, MiG_15bis,
MiG_19P, MiG_19P,
MiG_21Bis, MiG_21Bis,
Tu_95MS,
Yak_40, Yak_40,
) )
from dcs.ships import ( from dcs.ships import (
@ -32,6 +33,8 @@ Russia_1965 = {
An_30M, An_30M,
Yak_40, Yak_40,
Tu_95MS,
A_50, A_50,
Mi_8MT, Mi_8MT,

View File

@ -15,6 +15,8 @@ from dcs.planes import (
Su_17M4, Su_17M4,
Su_24M, Su_24M,
Su_25, Su_25,
Tu_22M3,
Tu_95MS,
Yak_40, Yak_40,
) )
from dcs.ships import ( from dcs.ships import (
@ -41,6 +43,9 @@ Russia_1975 = {
Su_24M, Su_24M,
Su_25, Su_25,
Tu_22M3,
Tu_95MS,
IL_76MD, IL_76MD,
IL_78M, IL_78M,
An_26B, An_26B,

View File

@ -17,6 +17,9 @@ from dcs.planes import (
Su_24M, Su_24M,
Su_25, Su_25,
Su_27, Su_27,
Tu_160,
Tu_22M3,
Tu_95MS,
Yak_40, Yak_40,
) )
from dcs.ships import ( from dcs.ships import (
@ -51,6 +54,10 @@ Russia_1990 = {
Su_25, Su_25,
Ka_50, Ka_50,
Tu_160,
Tu_22M3,
Tu_95MS,
IL_76MD, IL_76MD,
IL_78M, IL_78M,
An_26B, An_26B,

View File

@ -20,6 +20,9 @@ from dcs.planes import (
Su_30, Su_30,
Su_33, Su_33,
Su_34, Su_34,
Tu_160,
Tu_22M3,
Tu_95MS,
Yak_40, Yak_40,
) )
from dcs.ships import ( from dcs.ships import (
@ -55,6 +58,10 @@ Russia_2010 = {
Su_24M, Su_24M,
L_39ZA, L_39ZA,
Tu_160,
Tu_22M3,
Tu_95MS,
IL_76MD, IL_76MD,
IL_78M, IL_78M,
An_26B, An_26B,

View File

@ -20,6 +20,9 @@ from dcs.planes import (
Su_30, Su_30,
Su_33, Su_33,
Su_34, Su_34,
Tu_160,
Tu_22M3,
Tu_95MS,
Yak_40, Yak_40,
) )
from dcs.ships import ( from dcs.ships import (
@ -58,6 +61,10 @@ Russia_2020 = {
Su_24M, Su_24M,
L_39ZA, L_39ZA,
Tu_160,
Tu_22M3,
Tu_95MS,
IL_76MD, IL_76MD,
IL_78M, IL_78M,
An_26B, An_26B,

View File

@ -6,10 +6,13 @@ from dcs.helicopters import (
UH_1H, UH_1H,
) )
from dcs.planes import ( from dcs.planes import (
B_1B,
B_52H,
C_130, C_130,
E_3A, E_3A,
FA_18C_hornet, FA_18C_hornet,
F_15C, F_15C,
F_15E,
F_16C_50, F_16C_50,
F_5E_3, F_5E_3,
KC130, KC130,
@ -38,11 +41,15 @@ US_Aggressors = {
"units": [ "units": [
F_15C, F_15C,
F_15E,
F_5E_3, F_5E_3,
FA_18C_hornet, FA_18C_hornet,
F_16C_50, F_16C_50,
Su_27, Su_27,
B_1B,
B_52H,
KC_135, KC_135,
KC130, KC130,
C_130, C_130,

View File

@ -1,4 +1,5 @@
from dcs.planes import ( from dcs.planes import (
B_52H,
C_130, C_130,
E_3A, E_3A,
F_86F_Sabre, F_86F_Sabre,
@ -25,6 +26,8 @@ USA_1955 = {
F_86F_Sabre, F_86F_Sabre,
P_51D, P_51D,
B_52H,
KC_135, KC_135,
KC130, KC130,
C_130, C_130,

View File

@ -2,6 +2,7 @@ from dcs.helicopters import (
UH_1H, UH_1H,
) )
from dcs.planes import ( from dcs.planes import (
B_52H,
C_130, C_130,
E_3A, E_3A,
F_86F_Sabre, F_86F_Sabre,
@ -28,6 +29,8 @@ USA_1960 = {
F_86F_Sabre, F_86F_Sabre,
P_51D, P_51D,
B_52H,
KC_135, KC_135,
KC130, KC130,
C_130, C_130,

View File

@ -5,9 +5,12 @@ from dcs.helicopters import (
from dcs.planes import ( from dcs.planes import (
AV8BNA, AV8BNA,
A_10A, A_10A,
B_1B,
B_52H,
C_130, C_130,
E_3A, E_3A,
FA_18C_hornet, FA_18C_hornet,
F_117A,
F_14B, F_14B,
F_15C, F_15C,
F_15E, F_15E,
@ -43,6 +46,10 @@ USA_1990 = {
A_10A, A_10A,
AV8BNA, AV8BNA,
B_1B,
B_52H,
F_117A,
KC_135, KC_135,
KC130, KC130,
C_130, C_130,

View File

@ -6,9 +6,12 @@ from dcs.planes import (
AV8BNA, AV8BNA,
A_10C, A_10C,
A_10C_2, A_10C_2,
B_1B,
B_52H,
C_130, C_130,
E_3A, E_3A,
FA_18C_hornet, FA_18C_hornet,
F_117A,
F_14B, F_14B,
F_15C, F_15C,
F_15E, F_15E,
@ -46,6 +49,10 @@ USA_2005 = {
AV8BNA, AV8BNA,
MQ_9_Reaper, MQ_9_Reaper,
B_1B,
B_52H,
F_117A,
KC_135, KC_135,
KC130, KC130,
C_130, C_130,

View File

@ -14,7 +14,7 @@ from dcs.translation import String
from dcs.triggers import TriggerStart from dcs.triggers import TriggerStart
from dcs.unittype import UnitType from dcs.unittype import UnitType
from gen import Conflict, VisualGenerator, FlightType from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA from gen.airfields import AIRFIELD_DATA
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
@ -28,10 +28,11 @@ from gen.kneeboard import KneeboardGenerator
from gen.radios import RadioFrequency, RadioRegistry from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry from gen.tacan import TacanRegistry
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from plugin import LuaPluginManager
from theater import ControlPoint from theater import ControlPoint
from .. import db from .. import db
from ..debriefing import Debriefing from ..debriefing import Debriefing
from plugin import LuaPluginManager
class Operation: class Operation:
attackers_starting_position = None # type: db.StartingPosition attackers_starting_position = None # type: db.StartingPosition
@ -74,7 +75,7 @@ class Operation:
self.departure_cp = departure_cp self.departure_cp = departure_cp
self.to_cp = to_cp self.to_cp = to_cp
self.is_quick = False self.is_quick = False
self.listOfPluginsScripts = [] self.plugin_scripts: List[str] = []
def units_of(self, country_name: str) -> List[UnitType]: def units_of(self, country_name: str) -> List[UnitType]:
return [] return []
@ -133,33 +134,37 @@ class Operation:
else: else:
self.defenders_starting_position = None self.defenders_starting_position = None
def injectLuaTrigger(self, luascript, comment = "LUA script"): def inject_lua_trigger(self, contents: str, comment: str) -> None:
trigger = TriggerStart(comment=comment) trigger = TriggerStart(comment=comment)
trigger.add_action(DoScript(String(luascript))) trigger.add_action(DoScript(String(contents)))
self.current_mission.triggerrules.triggers.append(trigger) self.current_mission.triggerrules.triggers.append(trigger)
def bypassPluginScript(self, pluginName, scriptFileMnemonic): def bypass_plugin_script(self, mnemonic: str) -> None:
self.listOfPluginsScripts.append(scriptFileMnemonic) self.plugin_scripts.append(mnemonic)
def injectPluginScript(self, pluginName, scriptFile, scriptFileMnemonic): def inject_plugin_script(self, plugin_mnemonic: str, script: str,
if not scriptFileMnemonic in self.listOfPluginsScripts: script_mnemonic: str) -> None:
self.listOfPluginsScripts.append(scriptFileMnemonic) if script_mnemonic in self.plugin_scripts:
logging.debug(
f"Skipping already loaded {script} for {plugin_mnemonic}"
)
plugin_path = Path("./resources/plugins",pluginName) self.plugin_scripts.append(script_mnemonic)
if scriptFile != None: plugin_path = Path("./resources/plugins", plugin_mnemonic)
scriptFile_path = Path(plugin_path, scriptFile)
if scriptFile_path.exists():
trigger = TriggerStart(comment="Load " + scriptFileMnemonic)
filename = scriptFile_path.resolve()
fileref = self.current_mission.map_resource.add_resource_file(filename)
trigger.add_action(DoScriptFile(fileref))
self.current_mission.triggerrules.triggers.append(trigger)
else:
logging.error(f"Cannot find script file {scriptFile} for plugin {pluginName}")
else: script_path = Path(plugin_path, script)
logging.debug(f"Skipping script file {scriptFile} for plugin {pluginName}") if not script_path.exists():
logging.error(
f"Cannot find {script_path} for plugin {plugin_mnemonic}"
)
return
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
filename = script_path.resolve()
fileref = self.current_mission.map_resource.add_resource_file(filename)
trigger.add_action(DoScriptFile(fileref))
self.current_mission.triggerrules.triggers.append(trigger)
def generate(self): def generate(self):
radio_registry = RadioRegistry() radio_registry = RadioRegistry()
@ -334,7 +339,7 @@ class Operation:
kneeboard_generator.add_flight(flight) kneeboard_generator.add_flight(flight)
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]: if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]:
flightType = flight.flight_type.name flightType = flight.flight_type.name
flightTarget = flight.targetPoint flightTarget = flight.package.target
if flightTarget: if flightTarget:
flightTargetName = None flightTargetName = None
flightTargetType = None flightTargetType = None
@ -453,8 +458,6 @@ dcsLiberation.TargetPoints = {
self.current_mission.triggerrules.triggers.append(trigger) self.current_mission.triggerrules.triggers.append(trigger)
# Inject Plugins Lua Scripts and data # Inject Plugins Lua Scripts and data
self.listOfPluginsScripts = []
for plugin in LuaPluginManager().getPlugins(): for plugin in LuaPluginManager().getPlugins():
plugin.injectScripts(self) plugin.injectScripts(self)
plugin.injectConfiguration(self) plugin.injectConfiguration(self)

View File

@ -64,7 +64,6 @@ from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings from game.settings import Settings
from game.utils import nm_to_meter from game.utils import nm_to_meter
from gen.airfields import RunwayData
from gen.airsupportgen import AirSupport from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit from gen.callsigns import create_group_callsign_from_unit
@ -75,11 +74,13 @@ from gen.flights.flight import (
FlightWaypointType, FlightWaypointType,
) )
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData
from theater import TheaterGroundObject from theater import TheaterGroundObject
from theater.controlpoint import ControlPoint, ControlPointType from theater.controlpoint import ControlPoint, ControlPointType
from .conflictgen import Conflict from .conflictgen import Conflict
from .flights.traveltime import PackageWaypointTiming, TotEstimator from .flights.traveltime import PackageWaypointTiming, TotEstimator
from .naming import namegen from .naming import namegen
from .runways import RunwayAssigner
WARM_START_HELI_AIRSPEED = 120 WARM_START_HELI_AIRSPEED = 120
WARM_START_HELI_ALT = 500 WARM_START_HELI_ALT = 500
@ -204,6 +205,9 @@ class ChannelAssignment:
class FlightData: class FlightData:
"""Details of a planned flight.""" """Details of a planned flight."""
#: The package that the flight belongs to.
package: Package
flight_type: FlightType flight_type: FlightType
#: All units in the flight. #: All units in the flight.
@ -236,14 +240,13 @@ class FlightData:
#: Map of radio frequencies to their assigned radio and channel, if any. #: Map of radio frequencies to their assigned radio and channel, if any.
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment]
#: Data concerning the target of a CAS/Strike/SEAD flight, or None else def __init__(self, package: Package, flight_type: FlightType,
targetPoint = None units: List[FlyingUnit], size: int, friendly: bool,
departure_delay: int, departure: RunwayData,
def __init__(self, flight_type: FlightType, units: List[FlyingUnit], arrival: RunwayData, divert: Optional[RunwayData],
size: int, friendly: bool, departure_delay: int, waypoints: List[FlightWaypoint],
departure: RunwayData, arrival: RunwayData, intra_flight_channel: RadioFrequency) -> None:
divert: Optional[RunwayData], waypoints: List[FlightWaypoint], self.package = package
intra_flight_channel: RadioFrequency, targetPoint: Optional) -> None:
self.flight_type = flight_type self.flight_type = flight_type
self.units = units self.units = units
self.size = size self.size = size
@ -256,7 +259,6 @@ class FlightData:
self.intra_flight_channel = intra_flight_channel self.intra_flight_channel = intra_flight_channel
self.frequency_to_channel_map = {} self.frequency_to_channel_map = {}
self.callsign = create_group_callsign_from_unit(self.units[0]) self.callsign = create_group_callsign_from_unit(self.units[0])
self.targetPoint = targetPoint
@property @property
def client_units(self) -> List[FlyingUnit]: def client_units(self) -> List[FlyingUnit]:
@ -574,12 +576,12 @@ class AircraftConflictGenerator:
return StartType.Warm return StartType.Warm
def _setup_group(self, group: FlyingGroup, for_task: Type[Task], def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
flight: Flight, dynamic_runways: Dict[str, RunwayData]): package: Package, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
did_load_loadout = False did_load_loadout = False
unit_type = group.units[0].unit_type unit_type = group.units[0].unit_type
if unit_type in db.PLANE_PAYLOAD_OVERRIDES: if unit_type in db.PLANE_PAYLOAD_OVERRIDES:
override_loadout = db.PLANE_PAYLOAD_OVERRIDES[unit_type]
# Clear pylons # Clear pylons
for p in group.units: for p in group.units:
p.pylons.clear() p.pylons.clear()
@ -622,9 +624,12 @@ class AircraftConflictGenerator:
# TODO: Support for different departure/arrival airfields. # TODO: Support for different departure/arrival airfields.
cp = flight.from_cp cp = flight.from_cp
fallback_runway = RunwayData(cp.full_name, runway_name="") fallback_runway = RunwayData(cp.full_name, runway_heading=0,
runway_name="")
if cp.cptype == ControlPointType.AIRBASE: if cp.cptype == ControlPointType.AIRBASE:
departure_runway = self.get_preferred_runway(flight.from_cp.airport) assigner = RunwayAssigner(self.game.conditions)
departure_runway = assigner.get_preferred_runway(
flight.from_cp.airport)
elif cp.is_fleet: elif cp.is_fleet:
departure_runway = dynamic_runways.get(cp.name, fallback_runway) departure_runway = dynamic_runways.get(cp.name, fallback_runway)
else: else:
@ -632,6 +637,7 @@ class AircraftConflictGenerator:
departure_runway = fallback_runway departure_runway = fallback_runway
self.flights.append(FlightData( self.flights.append(FlightData(
package=package,
flight_type=flight.flight_type, flight_type=flight.flight_type,
units=group.units, units=group.units,
size=len(group.units), size=len(group.units),
@ -643,8 +649,7 @@ class AircraftConflictGenerator:
divert=None, divert=None,
# Waypoints are added later, after they've had their TOTs set. # Waypoints are added later, after they've had their TOTs set.
waypoints=[], waypoints=[],
intra_flight_channel=channel, intra_flight_channel=channel
targetPoint=flight.targetPoint,
)) ))
# Special case so Su 33 carrier take off # Special case so Su 33 carrier take off
@ -656,22 +661,6 @@ class AircraftConflictGenerator:
for unit in group.units: for unit in group.units:
unit.fuel = Su_33.fuel_max * 0.8 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, def _generate_at_airport(self, name: str, side: Country,
unit_type: FlyingType, count: int, start_type: str, unit_type: FlyingType, count: int, start_type: str,
airport: Optional[Airport] = None) -> FlyingGroup: airport: Optional[Airport] = None) -> FlyingGroup:
@ -802,7 +791,7 @@ class AircraftConflictGenerator:
logging.info(f"Generating flight: {flight.unit_type}") logging.info(f"Generating flight: {flight.unit_type}")
group = self.generate_planned_flight(flight.from_cp, country, group = self.generate_planned_flight(flight.from_cp, country,
flight) flight)
self.setup_flight_group(group, flight, dynamic_runways) self.setup_flight_group(group, package, flight, dynamic_runways)
self.create_waypoints(group, package, flight, timing) self.create_waypoints(group, package, flight, timing)
def set_activation_time(self, flight: Flight, group: FlyingGroup, def set_activation_time(self, flight: Flight, group: FlyingGroup,
@ -919,10 +908,11 @@ class AircraftConflictGenerator:
if flight.unit_type.eplrs: if flight.unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id)) group.points[0].tasks.append(EPLRS(group.id))
def configure_cap(self, group: FlyingGroup, flight: Flight, def configure_cap(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = CAP.name group.task = CAP.name
self._setup_group(group, CAP, flight, dynamic_runways) self._setup_group(group, CAP, package, flight, dynamic_runways)
if flight.unit_type not in GUNFIGHTERS: if flight.unit_type not in GUNFIGHTERS:
ammo_type = OptRTBOnOutOfAmmo.Values.AAM ammo_type = OptRTBOnOutOfAmmo.Values.AAM
@ -934,10 +924,11 @@ class AircraftConflictGenerator:
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50),
targets=[Targets.All.Air])) targets=[Targets.All.Air]))
def configure_cas(self, group: FlyingGroup, flight: Flight, def configure_cas(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = CAS.name group.task = CAS.name
self._setup_group(group, CAS, flight, dynamic_runways) self._setup_group(group, CAS, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
@ -949,10 +940,11 @@ class AircraftConflictGenerator:
targets=[Targets.All.GroundUnits.GroundVehicles]) targets=[Targets.All.GroundUnits.GroundVehicles])
) )
def configure_sead(self, group: FlyingGroup, flight: Flight, def configure_sead(self, group: FlyingGroup, package: Package,
dynamic_runways: Dict[str, RunwayData]) -> None: flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = SEAD.name group.task = SEAD.name
self._setup_group(group, SEAD, flight, dynamic_runways) self._setup_group(group, SEAD, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
@ -960,33 +952,37 @@ class AircraftConflictGenerator:
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM, rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
restrict_jettison=True) restrict_jettison=True)
def configure_strike(self, group: FlyingGroup, flight: Flight, def configure_strike(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = PinpointStrike.name group.task = PinpointStrike.name
self._setup_group(group, GroundAttack, flight, dynamic_runways) self._setup_group(group, GroundAttack, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire, roe=OptROE.Values.OpenFire,
restrict_jettison=True) restrict_jettison=True)
def configure_anti_ship(self, group: FlyingGroup, flight: Flight, def configure_anti_ship(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = AntishipStrike.name group.task = AntishipStrike.name
self._setup_group(group, AntishipStrike, flight, dynamic_runways) self._setup_group(group, AntishipStrike, package, flight,
dynamic_runways)
self.configure_behavior( self.configure_behavior(
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire, roe=OptROE.Values.OpenFire,
restrict_jettison=True) restrict_jettison=True)
def configure_escort(self, group: FlyingGroup, flight: Flight, def configure_escort(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
# Escort groups are actually given the CAP task so they can perform the # Escort groups are actually given the CAP task so they can perform the
# Search Then Engage task, which we have to use instead of the Escort # Search Then Engage task, which we have to use instead of the Escort
# task for the reasons explained in JoinPointBuilder. # task for the reasons explained in JoinPointBuilder.
group.task = CAP.name group.task = CAP.name
self._setup_group(group, CAP, flight, dynamic_runways) self._setup_group(group, CAP, package, flight, dynamic_runways)
self.configure_behavior(group, roe=OptROE.Values.OpenFire, self.configure_behavior(group, roe=OptROE.Values.OpenFire,
restrict_jettison=True) restrict_jettison=True)
@ -995,22 +991,23 @@ class AircraftConflictGenerator:
logging.error(f"Unhandled flight type: {flight.flight_type.name}") logging.error(f"Unhandled flight type: {flight.flight_type.name}")
self.configure_behavior(group) self.configure_behavior(group)
def setup_flight_group(self, group: FlyingGroup, flight: Flight, def setup_flight_group(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
flight_type = flight.flight_type flight_type = flight.flight_type
if flight_type in [FlightType.BARCAP, FlightType.TARCAP, if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
FlightType.INTERCEPTION]: FlightType.INTERCEPTION]:
self.configure_cap(group, flight, dynamic_runways) self.configure_cap(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]: elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, flight, dynamic_runways) self.configure_cas(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.SEAD, FlightType.DEAD]: elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
self.configure_sead(group, flight, dynamic_runways) self.configure_sead(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.STRIKE]: elif flight_type in [FlightType.STRIKE]:
self.configure_strike(group, flight, dynamic_runways) self.configure_strike(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.ANTISHIP]: elif flight_type in [FlightType.ANTISHIP]:
self.configure_anti_ship(group, flight, dynamic_runways) self.configure_anti_ship(group, package, flight, dynamic_runways)
elif flight_type == FlightType.ESCORT: elif flight_type == FlightType.ESCORT:
self.configure_escort(group, flight, dynamic_runways) self.configure_escort(group, package, flight, dynamic_runways)
else: else:
self.configure_unknown_task(group, flight) self.configure_unknown_task(group, flight)
@ -1301,8 +1298,9 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
pattern=OrbitAction.OrbitPattern.RaceTrack pattern=OrbitAction.OrbitPattern.RaceTrack
)) ))
self.set_waypoint_tot(waypoint, self.timing.race_track_start) self.set_waypoint_tot(waypoint,
racetrack.stop_after_time(self.timing.race_track_end) self.timing.race_track_start(self.flight))
racetrack.stop_after_time(self.timing.race_track_end(self.flight))
waypoint.add_task(racetrack) waypoint.add_task(racetrack)
return waypoint return waypoint

View File

@ -3,11 +3,11 @@
Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing
data added to pydcs. Until then, missing data can be manually filled in here. data added to pydcs. Until then, missing data can be manually filled in here.
""" """
from dataclasses import dataclass, field from __future__ import annotations
import logging
from typing import Dict, Iterator, Optional, Tuple from dataclasses import dataclass, field
from typing import Dict, Optional, Tuple
from dcs.terrain.terrain import Airport
from .radios import MHz, RadioFrequency from .radios import MHz, RadioFrequency
from .tacan import TacanBand, TacanChannel from .tacan import TacanBand, TacanChannel
@ -1503,61 +1503,3 @@ AIRFIELD_DATA = {
atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)),
), ),
} }
@dataclass(frozen=True)
class RunwayData:
airfield_name: str
runway_name: str
atc: Optional[RadioFrequency] = None
tacan: Optional[TacanChannel] = None
tacan_callsign: Optional[str] = None
ils: Optional[RadioFrequency] = None
icls: Optional[int] = None
@classmethod
def for_airfield(cls, airport: Airport, runway: str) -> "RunwayData":
"""Creates RunwayData for the given runway of an airfield.
Args:
airport: The airfield the runway belongs to.
runway: Identifier of the runway to use. e.g. "03" or "20L".
"""
atc: Optional[RadioFrequency] = None
tacan: Optional[TacanChannel] = None
tacan_callsign: Optional[str] = None
ils: Optional[RadioFrequency] = None
try:
airfield = AIRFIELD_DATA[airport.name]
if airfield.atc is not None:
atc = airfield.atc.uhf
else:
atc = None
tacan = airfield.tacan
tacan_callsign = airfield.tacan_callsign
ils = airfield.ils_freq(runway)
except KeyError:
logging.warning(f"No airfield data for {airport.name}")
return cls(
airfield_name=airport.name,
runway_name=runway,
atc=atc,
tacan=tacan,
tacan_callsign=tacan_callsign,
ils=ils
)
@classmethod
def for_pydcs_airport(cls, airport: Airport) -> Iterator["RunwayData"]:
for runway in airport.runways:
runway_number = runway.heading // 10
runway_side = ["", "L", "R"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, runway_name)
# pydcs only exposes one runway per physical runway, so to expose
# both sides of the runway we need to generate the other.
runway_number = ((runway.heading + 180) % 360) // 10
runway_side = ["", "R", "L"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, runway_name)

View File

@ -1,19 +1,20 @@
import datetime import datetime
import os import os
import random
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
import random
from typing import List from typing import List
from game import db
from dcs.mission import Mission from dcs.mission import Mission
from game import db
from .aircraft import FlightData from .aircraft import FlightData
from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo from .airsupportgen import AwacsInfo, TankerInfo
from .armor import JtacInfo from .armor import JtacInfo
from .conflictgen import Conflict from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency from .radios import RadioFrequency
from .runways import RunwayData
@dataclass @dataclass

View File

@ -206,10 +206,9 @@ class PackageBuilder:
if assignment is None: if assignment is None:
return False return False
airfield, aircraft = assignment airfield, aircraft = assignment
flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task, flight = Flight(self.package, aircraft, plan.num_aircraft, airfield,
self.start_type) plan.task, self.start_type)
self.package.add_flight(flight) self.package.add_flight(flight)
flight.targetPoint = self.package.target
return True return True
def build(self) -> Package: def build(self) -> Package:
@ -222,7 +221,7 @@ class PackageBuilder:
for flight in flights: for flight in flights:
self.global_inventory.return_from_flight(flight) self.global_inventory.return_from_flight(flight)
self.package.remove_flight(flight) self.package.remove_flight(flight)
flight.targetPoint = None
class ObjectiveFinder: class ObjectiveFinder:
"""Identifies potential objectives for the mission planner.""" """Identifies potential objectives for the mission planner."""

View File

@ -19,11 +19,14 @@ from dcs.planes import (
A_10C_2, A_10C_2,
A_20G, A_20G,
B_17G, B_17G,
B_1B,
B_52H,
Bf_109K_4, Bf_109K_4,
C_101CC, C_101CC,
FA_18C_hornet, FA_18C_hornet,
FW_190A8, FW_190A8,
FW_190D9, FW_190D9,
F_117A,
F_14B, F_14B,
F_15C, F_15C,
F_15E, F_15E,
@ -71,6 +74,9 @@ from dcs.planes import (
Su_34, Su_34,
Tornado_GR4, Tornado_GR4,
Tornado_IDS, Tornado_IDS,
Tu_160,
Tu_22M3,
Tu_95MS,
WingLoong_I, WingLoong_I,
) )
@ -226,6 +232,8 @@ CAS_CAPABLE = [
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
B_1B,
Tornado_IDS, Tornado_IDS,
Tornado_GR4, Tornado_GR4,
@ -367,6 +375,10 @@ STRIKE_CAPABLE = [
Su_25T, Su_25T,
Su_34, Su_34,
Tu_160,
Tu_22M3,
Tu_95MS,
JF_17, JF_17,
M_2000C, M_2000C,
@ -384,6 +396,10 @@ STRIKE_CAPABLE = [
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
B_1B,
B_52H,
F_117A,
Tornado_IDS, Tornado_IDS,
Tornado_GR4, Tornado_GR4,
@ -413,11 +429,16 @@ STRIKE_CAPABLE = [
STRIKE_PREFERRED = [ STRIKE_PREFERRED = [
AJS37, AJS37,
F_15E,
Tornado_GR4,
A_20G, A_20G,
B_17G, B_17G,
B_1B,
B_52H,
F_117A,
F_15E,
Tornado_GR4,
Tu_160,
Tu_22M3,
Tu_95MS,
] ]
ANTISHIP_CAPABLE = [ ANTISHIP_CAPABLE = [

View File

@ -1,5 +1,7 @@
from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Dict, Iterable, List, Optional from typing import Dict, Iterable, List, Optional, TYPE_CHECKING
from dcs.mapping import Point from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
@ -8,6 +10,9 @@ from dcs.unittype import UnitType
from game import db from game import db
from theater.controlpoint import ControlPoint, MissionTarget from theater.controlpoint import ControlPoint, MissionTarget
if TYPE_CHECKING:
from gen.ato import Package
class FlightType(Enum): class FlightType(Enum):
CAP = 0 # Do not use. Use BARCAP or TARCAP. CAP = 0 # Do not use. Use BARCAP or TARCAP.
@ -138,10 +143,11 @@ class Flight:
use_custom_loadout = False use_custom_loadout = False
preset_loadout_name = "" preset_loadout_name = ""
group = False # Contains DCS Mission group data after mission has been generated group = False # Contains DCS Mission group data after mission has been generated
targetPoint = None # Contains either None or a Strike/SEAD target point location
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint, def __init__(self, package: Package, unit_type: UnitType, count: int,
flight_type: FlightType, start_type: str) -> None: from_cp: ControlPoint, flight_type: FlightType,
start_type: str) -> None:
self.package = package
self.unit_type = unit_type self.unit_type = unit_type
self.count = count self.count = count
self.from_cp = from_cp self.from_cp = from_cp

View File

@ -132,7 +132,7 @@ class FlightPlanBuilder:
if not isinstance(location, TheaterGroundObject): if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -157,11 +157,7 @@ class FlightPlanBuilder:
if building.is_dead: if building.is_dead:
continue continue
builder.strike_point( builder.strike_point(building, building.category, location)
building,
f"{building.obj_name} {building.category}",
location
)
builder.egress(self.package.waypoints.egress, location) builder.egress(self.package.waypoints.egress, location)
builder.split(self.package.waypoints.split) builder.split(self.package.waypoints.split)
@ -222,7 +218,7 @@ class FlightPlanBuilder:
) )
start = end.point_from_heading(heading - 180, diameter) start = end.point_from_heading(heading - 180, diameter)
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.race_track(start, end, patrol_alt) builder.race_track(start, end, patrol_alt)
builder.rtb(flight.from_cp) builder.rtb(flight.from_cp)
@ -264,7 +260,7 @@ class FlightPlanBuilder:
orbit1p = orbit_center.point_from_heading(heading + 180, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius)
# Create points # Create points
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -290,7 +286,7 @@ class FlightPlanBuilder:
if custom_targets is None: if custom_targets is None:
custom_targets = [] custom_targets = []
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -328,7 +324,7 @@ class FlightPlanBuilder:
def generate_escort(self, flight: Flight) -> None: def generate_escort(self, flight: Flight) -> None:
assert self.package.waypoints is not None assert self.package.waypoints is not None
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
@ -351,9 +347,6 @@ class FlightPlanBuilder:
if not isinstance(location, FrontLine): if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
is_helo = getattr(flight.unit_type, "helicopter", False)
cap_alt = 500 if is_helo else 1000
ingress, heading, distance = Conflict.frontline_vector( ingress, heading, distance = Conflict.frontline_vector(
location.control_points[0], location.control_points[1], location.control_points[0], location.control_points[1],
self.game.theater self.game.theater
@ -361,15 +354,15 @@ class FlightPlanBuilder:
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance) egress = ingress.point_from_heading(heading, distance)
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(flight.from_cp, is_helo) builder.ascent(flight.from_cp)
builder.hold(self._hold_point(flight)) builder.hold(self._hold_point(flight))
builder.join(self.package.waypoints.join) builder.join(self.package.waypoints.join)
builder.ingress_cas(ingress, location) builder.ingress_cas(ingress, location)
builder.cas(center, cap_alt) builder.cas(center)
builder.egress(egress, location) builder.egress(egress, location)
builder.split(self.package.waypoints.split) builder.split(self.package.waypoints.split)
builder.rtb(flight.from_cp, is_helo) builder.rtb(flight.from_cp)
flight.points = builder.build() flight.points = builder.build()
@ -382,7 +375,7 @@ class FlightPlanBuilder:
flight: The flight to generate the descend point for. flight: The flight to generate the descend point for.
departure: Departure airfield or carrier. departure: Departure airfield or carrier.
""" """
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.ascent(departure) builder.ascent(departure)
return builder.build()[0] return builder.build()[0]
@ -394,7 +387,7 @@ class FlightPlanBuilder:
flight: The flight to generate the descend point for. flight: The flight to generate the descend point for.
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.descent(arrival) builder.descent(arrival)
return builder.build()[0] return builder.build()[0]
@ -406,7 +399,7 @@ class FlightPlanBuilder:
flight: The flight to generate the landing waypoint for. flight: The flight to generate the landing waypoint for.
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
builder = WaypointBuilder(flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder.land(arrival) builder.land(arrival)
return builder.build()[0] return builder.build()[0]

View File

@ -27,21 +27,22 @@ INGRESS_TYPES = {
FlightWaypointType.INGRESS_STRIKE, FlightWaypointType.INGRESS_STRIKE,
} }
IP_TYPES = {
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
FlightWaypointType.PATROL_TRACK,
}
class GroundSpeed: class GroundSpeed:
@staticmethod @staticmethod
def mission_speed(package: Package) -> int: def mission_speed(package: Package) -> int:
speeds = set() speeds = set()
for flight in package.flights: for flight in package.flights:
waypoint = flight.waypoint_with_type(IP_TYPES) # Find a waypoint that matches the mission start waypoint and use
# that for the altitude of the mission. That may not be true for the
# whole mission, but it's probably good enough for now.
waypoint = flight.waypoint_with_type({
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
FlightWaypointType.PATROL_TRACK,
})
if waypoint is None: if waypoint is None:
logging.error(f"Could not find ingress point for {flight}.") logging.error(f"Could not find ingress point for {flight}.")
if flight.points: if flight.points:
@ -152,8 +153,10 @@ class TotEstimator:
# Takeoff immediately. # Takeoff immediately.
return 0 return 0
if self.package.primary_task == FlightType.BARCAP: # BARCAP flights do not coordinate with the rest of the package on join
start_time = self.timing.race_track_start # or ingress points.
if flight.flight_type == FlightType.BARCAP:
start_time = self.timing.race_track_start(flight)
else: else:
start_time = self.timing.join start_time = self.timing.join
return start_time - travel_time - self.HOLD_TIME return start_time - travel_time - self.HOLD_TIME
@ -166,7 +169,9 @@ class TotEstimator:
def earliest_tot_for_flight(self, flight: Flight) -> int: def earliest_tot_for_flight(self, flight: Flight) -> int:
"""Estimate fastest time from mission start to the target position. """Estimate fastest time from mission start to the target position.
For CAP missions, this is time to race track start. For BARCAP flights, this is time to race track start. This ensures that
they are on station at the same time any other package members reach
their ingress point.
For other mission types this is the time to the mission target. For other mission types this is the time to the mission target.
@ -177,27 +182,34 @@ class TotEstimator:
The earliest possible TOT for the given flight in seconds. Returns 0 The earliest possible TOT for the given flight in seconds. Returns 0
if an ingress point cannot be found. if an ingress point cannot be found.
""" """
time_to_ingress = self.estimate_waypoints_to_target(flight, IP_TYPES) if flight.flight_type == FlightType.BARCAP:
if time_to_ingress is None: time_to_target = self.estimate_waypoints_to_target(flight, {
logging.warning( FlightWaypointType.PATROL_TRACK
f"Found no ingress types. Cannot estimate TOT for {flight}") })
# Return 0 so this flight's travel time does not affect the rest of if time_to_target is None:
# the package. logging.warning(
return 0 f"Found no race track. Cannot estimate TOT for {flight}")
# Return 0 so this flight's travel time does not affect the rest
if self.package.primary_task == FlightType.BARCAP: # of the package.
# The racetrack start *is* the target. The package target is the return 0
# protected objective.
time_to_target = 0
else: else:
time_to_ingress = self.estimate_waypoints_to_target(
flight, INGRESS_TYPES
)
if time_to_ingress is None:
logging.warning(
f"Found no ingress types. Cannot estimate TOT for {flight}")
# Return 0 so this flight's travel time does not affect the rest
# of the package.
return 0
assert self.package.waypoints is not None assert self.package.waypoints is not None
time_to_target = TravelTime.between_points( time_to_target = time_to_ingress + TravelTime.between_points(
self.package.waypoints.ingress, self.package.target.position, self.package.waypoints.ingress, self.package.target.position,
GroundSpeed.mission_speed(self.package)) GroundSpeed.mission_speed(self.package))
return sum([ return sum([
self.estimate_startup(flight), self.estimate_startup(flight),
self.estimate_ground_ops(flight), self.estimate_ground_ops(flight),
time_to_ingress,
time_to_target, time_to_target,
]) ])
@ -281,18 +293,22 @@ class PackageWaypointTiming:
assert self.package.time_over_target is not None assert self.package.time_over_target is not None
return self.package.time_over_target return self.package.time_over_target
@property def race_track_start(self, flight: Flight) -> int:
def race_track_start(self) -> int: if flight.flight_type == FlightType.BARCAP:
if self.package.primary_task == FlightType.BARCAP: return self.target
return self.package.time_over_target
else: else:
# The only other type that (currently) uses race tracks is TARCAP,
# which is sort of in need of cleanup. TARCAP is only valid on front
# lines and they participate in join points and patrol between the
# ingress and egress points rather than on a race track actually
# pointed at the enemy.
return self.ingress return self.ingress
@property def race_track_end(self, flight: Flight) -> int:
def race_track_end(self) -> int: if flight.flight_type == FlightType.BARCAP:
if self.package.primary_task == FlightType.BARCAP:
return self.target + CAP_DURATION * 60 return self.target + CAP_DURATION * 60
else: else:
# For TARCAP. See the explanation in race_track_start.
return self.egress return self.egress
def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int: def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int:
@ -303,7 +319,8 @@ class PackageWaypointTiming:
GroundSpeed.for_flight(flight, hold_point.alt) GroundSpeed.for_flight(flight, hold_point.alt)
) )
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[int]: def tot_for_waypoint(self, flight: Flight,
waypoint: FlightWaypoint) -> Optional[int]:
target_types = ( target_types = (
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
FlightWaypointType.TARGET_POINT, FlightWaypointType.TARGET_POINT,
@ -321,7 +338,7 @@ class PackageWaypointTiming:
elif waypoint.waypoint_type == FlightWaypointType.SPLIT: elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
return self.split return self.split
elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK: elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK:
return self.race_track_start return self.race_track_start(flight)
return None return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint, def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
@ -329,7 +346,7 @@ class PackageWaypointTiming:
if waypoint.waypoint_type == FlightWaypointType.LOITER: if waypoint.waypoint_type == FlightWaypointType.LOITER:
return self.push_time(flight, waypoint) return self.push_time(flight, waypoint)
elif waypoint.waypoint_type == FlightWaypointType.PATROL: elif waypoint.waypoint_type == FlightWaypointType.PATROL:
return self.race_track_end return self.race_track_end(flight)
return None return None
@classmethod @classmethod

View File

@ -7,29 +7,35 @@ from dcs.unit import Unit
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.utils import nm_to_meter from game.utils import nm_to_meter
from game.weather import Conditions
from theater import ControlPoint, MissionTarget, TheaterGroundObject from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightWaypoint, FlightWaypointType
from ..runways import RunwayAssigner
class WaypointBuilder: class WaypointBuilder:
def __init__(self, flight: Flight, doctrine: Doctrine) -> None: def __init__(self, conditions: Conditions, flight: Flight,
doctrine: Doctrine) -> None:
self.conditions = conditions
self.flight = flight self.flight = flight
self.doctrine = doctrine self.doctrine = doctrine
self.waypoints: List[FlightWaypoint] = [] self.waypoints: List[FlightWaypoint] = []
self.ingress_point: Optional[FlightWaypoint] = None self.ingress_point: Optional[FlightWaypoint] = None
@property
def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False)
def build(self) -> List[FlightWaypoint]: def build(self) -> List[FlightWaypoint]:
return self.waypoints return self.waypoints
def ascent(self, departure: ControlPoint, is_helo: bool = False) -> None: def ascent(self, departure: ControlPoint) -> None:
"""Create ascent waypoint for the given departure airfield or carrier. """Create ascent waypoint for the given departure airfield or carrier.
Args: Args:
departure: Departure airfield or carrier. departure: Departure airfield or carrier.
is_helo: True if the flight is a helicopter.
""" """
# TODO: Pick runway based on wind direction. heading = RunwayAssigner(self.conditions).takeoff_heading(departure)
heading = departure.heading
position = departure.position.point_from_heading( position = departure.position.point_from_heading(
heading, nm_to_meter(5) heading, nm_to_meter(5)
) )
@ -37,7 +43,7 @@ class WaypointBuilder:
FlightWaypointType.ASCEND_POINT, FlightWaypointType.ASCEND_POINT,
position.x, position.x,
position.y, position.y,
500 if is_helo else self.doctrine.pattern_altitude 500 if self.is_helo else self.doctrine.pattern_altitude
) )
waypoint.name = "ASCEND" waypoint.name = "ASCEND"
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@ -45,16 +51,15 @@ class WaypointBuilder:
waypoint.pretty_name = "Ascend" waypoint.pretty_name = "Ascend"
self.waypoints.append(waypoint) self.waypoints.append(waypoint)
def descent(self, arrival: ControlPoint, is_helo: bool = False) -> None: def descent(self, arrival: ControlPoint) -> None:
"""Create descent waypoint for the given arrival airfield or carrier. """Create descent waypoint for the given arrival airfield or carrier.
Args: Args:
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
is_helo: True if the flight is a helicopter.
""" """
# TODO: Pick runway based on wind direction. landing_heading = RunwayAssigner(self.conditions).landing_heading(
# ControlPoint.heading is the departure heading. arrival)
heading = (arrival.heading + 180) % 360 heading = (landing_heading + 180) % 360
position = arrival.position.point_from_heading( position = arrival.position.point_from_heading(
heading, nm_to_meter(5) heading, nm_to_meter(5)
) )
@ -62,7 +67,7 @@ class WaypointBuilder:
FlightWaypointType.DESCENT_POINT, FlightWaypointType.DESCENT_POINT,
position.x, position.x,
position.y, position.y,
300 if is_helo else self.doctrine.pattern_altitude 300 if self.is_helo else self.doctrine.pattern_altitude
) )
waypoint.name = "DESCEND" waypoint.name = "DESCEND"
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@ -94,7 +99,7 @@ class WaypointBuilder:
FlightWaypointType.LOITER, FlightWaypointType.LOITER,
position.x, position.x,
position.y, position.y,
self.doctrine.rendezvous_altitude 500 if self.is_helo else self.doctrine.rendezvous_altitude
) )
waypoint.pretty_name = "Hold" waypoint.pretty_name = "Hold"
waypoint.description = "Wait until push time" waypoint.description = "Wait until push time"
@ -106,7 +111,7 @@ class WaypointBuilder:
FlightWaypointType.JOIN, FlightWaypointType.JOIN,
position.x, position.x,
position.y, position.y,
self.doctrine.ingress_altitude 500 if self.is_helo else self.doctrine.ingress_altitude
) )
waypoint.pretty_name = "Join" waypoint.pretty_name = "Join"
waypoint.description = "Rendezvous with package" waypoint.description = "Rendezvous with package"
@ -118,7 +123,7 @@ class WaypointBuilder:
FlightWaypointType.SPLIT, FlightWaypointType.SPLIT,
position.x, position.x,
position.y, position.y,
self.doctrine.ingress_altitude 500 if self.is_helo else self.doctrine.ingress_altitude
) )
waypoint.pretty_name = "Split" waypoint.pretty_name = "Split"
waypoint.description = "Depart from package" waypoint.description = "Depart from package"
@ -146,7 +151,7 @@ class WaypointBuilder:
ingress_type, ingress_type,
position.x, position.x,
position.y, position.y,
self.doctrine.ingress_altitude 500 if self.is_helo else self.doctrine.ingress_altitude
) )
waypoint.pretty_name = "INGRESS on " + objective.name waypoint.pretty_name = "INGRESS on " + objective.name
waypoint.description = "INGRESS on " + objective.name waypoint.description = "INGRESS on " + objective.name
@ -159,7 +164,7 @@ class WaypointBuilder:
FlightWaypointType.EGRESS, FlightWaypointType.EGRESS,
position.x, position.x,
position.y, position.y,
self.doctrine.ingress_altitude 500 if self.is_helo else self.doctrine.ingress_altitude
) )
waypoint.pretty_name = "EGRESS from " + target.name waypoint.pretty_name = "EGRESS from " + target.name
waypoint.description = "EGRESS from " + target.name waypoint.description = "EGRESS from " + target.name
@ -168,24 +173,21 @@ class WaypointBuilder:
def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str, def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None: location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE [{location.name}]: {name}", self._target_point(target, name, f"STRIKE {name}", location)
location)
# TODO: Seems fishy. # TODO: Seems fishy.
if self.ingress_point is not None: if self.ingress_point is not None:
self.ingress_point.targetGroup = location self.ingress_point.targetGroup = location
def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str, def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None: location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE [{location.name}]: {name}", self._target_point(target, name, f"STRIKE {name}", location)
location)
# TODO: Seems fishy. # TODO: Seems fishy.
if self.ingress_point is not None: if self.ingress_point is not None:
self.ingress_point.targetGroup = location self.ingress_point.targetGroup = location
def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str, def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None: location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE [{location.name}]: {name}", self._target_point(target, name, f"STRIKE {name}", location)
location)
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str, def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
description: str, location: MissionTarget) -> None: description: str, location: MissionTarget) -> None:
@ -246,12 +248,12 @@ class WaypointBuilder:
# TODO: This seems wrong, but it's what was there before. # TODO: This seems wrong, but it's what was there before.
self.ingress_point.targets.append(location) self.ingress_point.targets.append(location)
def cas(self, position: Point, altitude: int) -> None: def cas(self, position: Point) -> None:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.CAS, FlightWaypointType.CAS,
position.x, position.x,
position.y, position.y,
altitude 500 if self.is_helo else 1000
) )
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
waypoint.description = "Provide CAS" waypoint.description = "Provide CAS"
@ -306,14 +308,13 @@ class WaypointBuilder:
self.race_track_start(start, altitude) self.race_track_start(start, altitude)
self.race_track_end(end, altitude) self.race_track_end(end, altitude)
def rtb(self, arrival: ControlPoint, is_helo: bool = False) -> None: def rtb(self, arrival: ControlPoint) -> None:
"""Creates descent ant landing waypoints for the given control point. """Creates descent ant landing waypoints for the given control point.
Args: Args:
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
is_helo: True if the flight is a helicopter.
""" """
self.descent(arrival, is_helo) self.descent(arrival)
self.land(arrival) self.land(arrival)
def escort(self, ingress: Point, target: MissionTarget, def escort(self, ingress: Point, target: MissionTarget,
@ -337,7 +338,7 @@ class WaypointBuilder:
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
target.position.x, target.position.x,
target.position.y, target.position.y,
self.doctrine.ingress_altitude 500 if self.is_helo else self.doctrine.ingress_altitude
) )
waypoint.name = "TARGET" waypoint.name = "TARGET"
waypoint.description = "Escort the package" waypoint.description = "Escort the package"

View File

@ -16,9 +16,9 @@ from dcs.unitgroup import StaticGroup
from game import db from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name from game.db import unit_type_from_name
from .airfields import RunwayData
from .conflictgen import Conflict from .conflictgen import Conflict
from .radios import RadioRegistry from .radios import RadioRegistry
from .runways import RunwayData
from .tacan import TacanBand, TacanRegistry from .tacan import TacanBand, TacanRegistry
FARP_FRONTLINE_DISTANCE = 10000 FARP_FRONTLINE_DISTANCE = 10000
@ -141,8 +141,9 @@ class GroundObjectsGenerator:
# Find carrier direction (In the wind) # Find carrier direction (In the wind)
found_carrier_destination = False found_carrier_destination = False
attempt = 0 attempt = 0
brc = self.m.weather.wind_at_ground.direction + 180
while not found_carrier_destination and attempt < 5: while not found_carrier_destination and attempt < 5:
point = sg.points[0].position.point_from_heading(self.m.weather.wind_at_ground.direction + 180, 100000-attempt*20000) point = sg.points[0].position.point_from_heading(brc, 100000-attempt*20000)
if self.game.theater.is_in_sea(point): if self.game.theater.is_in_sea(point):
found_carrier_destination = True found_carrier_destination = True
sg.add_waypoint(point) sg.add_waypoint(point)
@ -196,6 +197,7 @@ class GroundObjectsGenerator:
# unit name since it's an arbitrary ID. # unit name since it's an arbitrary ID.
self.runways[cp.name] = RunwayData( self.runways[cp.name] = RunwayData(
cp.name, cp.name,
brc,
"N/A", "N/A",
atc=atc_channel, atc=atc_channel,
tacan=tacan, tacan=tacan,

View File

@ -22,14 +22,13 @@ 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 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. aircraft will be able to see the enemy's kneeboard for the same airframe.
""" """
import datetime
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from dcs.mapping import Point
from dcs.mission import Mission from dcs.mission import Mission
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from tabulate import tabulate from tabulate import tabulate
@ -37,12 +36,11 @@ from tabulate import tabulate
from game.utils import meter_to_nm from game.utils import meter_to_nm
from . import units from . import units
from .aircraft import AIRCRAFT_DATA, FlightData from .aircraft import AIRCRAFT_DATA, FlightData
from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from .flights.flight import FlightWaypoint, FlightWaypointType from .flights.flight import FlightWaypoint, FlightWaypointType
from .flights.traveltime import TravelTime
from .radios import RadioFrequency from .radios import RadioFrequency
from .runways import RunwayData
class KneeboardPageWriter: class KneeboardPageWriter:
@ -126,6 +124,7 @@ class FlightPlanBuilder:
self.target_points = [] self.target_points = []
self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint)) self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint))
self.last_waypoint = waypoint
def coalesce_target_points(self) -> None: def coalesce_target_points(self) -> None:
if len(self.target_points) <= 4: if len(self.target_points) <= 4:
@ -157,7 +156,6 @@ class FlightPlanBuilder:
self._format_time(waypoint.waypoint.tot), self._format_time(waypoint.waypoint.tot),
self._format_time(waypoint.waypoint.departure_time), self._format_time(waypoint.waypoint.departure_time),
]) ])
self.last_waypoint = waypoint.waypoint
def _format_time(self, time: Optional[int]) -> str: def _format_time(self, time: Optional[int]) -> str:
if time is None: if time is None:

139
gen/runways.py Normal file
View File

@ -0,0 +1,139 @@
"""Runway information and selection."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Iterator, Optional
from dcs.terrain.terrain import Airport
from game.weather import Conditions
from theater import ControlPoint, ControlPointType
from .airfields import AIRFIELD_DATA
from .radios import RadioFrequency
from .tacan import TacanChannel
@dataclass(frozen=True)
class RunwayData:
airfield_name: str
runway_heading: int
runway_name: str
atc: Optional[RadioFrequency] = None
tacan: Optional[TacanChannel] = None
tacan_callsign: Optional[str] = None
ils: Optional[RadioFrequency] = None
icls: Optional[int] = None
@classmethod
def for_airfield(cls, airport: Airport, runway_heading: int,
runway_name: str) -> RunwayData:
"""Creates RunwayData for the given runway of an airfield.
Args:
airport: The airfield the runway belongs to.
runway_heading: Heading of the runway.
runway_name: Identifier of the runway to use. e.g. "03" or "20L".
"""
atc: Optional[RadioFrequency] = None
tacan: Optional[TacanChannel] = None
tacan_callsign: Optional[str] = None
ils: Optional[RadioFrequency] = None
try:
airfield = AIRFIELD_DATA[airport.name]
if airfield.atc is not None:
atc = airfield.atc.uhf
else:
atc = None
tacan = airfield.tacan
tacan_callsign = airfield.tacan_callsign
ils = airfield.ils_freq(runway_name)
except KeyError:
logging.warning(f"No airfield data for {airport.name}")
return cls(
airfield_name=airport.name,
runway_heading=runway_heading,
runway_name=runway_name,
atc=atc,
tacan=tacan,
tacan_callsign=tacan_callsign,
ils=ils
)
@classmethod
def for_pydcs_airport(cls, airport: Airport) -> Iterator[RunwayData]:
for runway in airport.runways:
runway_number = runway.heading // 10
runway_side = ["", "L", "R"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, runway.heading, runway_name)
# pydcs only exposes one runway per physical runway, so to expose
# both sides of the runway we need to generate the other.
heading = (runway.heading + 180) % 360
runway_number = heading // 10
runway_side = ["", "R", "L"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, heading, runway_name)
class RunwayAssigner:
def __init__(self, conditions: Conditions):
self.conditions = conditions
def angle_off_headwind(self, runway: RunwayData) -> int:
wind = self.conditions.weather.wind.at_0m.direction
ideal_heading = (wind + 180) % 360
return abs(runway.runway_heading - ideal_heading)
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))
# Find the runway with the best headwind first.
best_runways = [runways[0]]
best_angle_off_headwind = self.angle_off_headwind(best_runways[0])
for runway in runways[1:]:
angle_off_headwind = self.angle_off_headwind(runway)
if angle_off_headwind == best_angle_off_headwind:
best_runways.append(runway)
elif angle_off_headwind < best_angle_off_headwind:
best_runways = [runway]
best_angle_off_headwind = angle_off_headwind
for runway in best_runways:
# But if there are multiple runways with the same heading,
# prefer
# and ILS capable runway.
if runway.ils is not None:
return runway
# Otherwise the only difference between the two is the distance from
# parking, which we don't know, so just pick the first one.
return best_runways[0]
def takeoff_heading(self, departure: ControlPoint) -> int:
if departure.cptype == ControlPointType.AIRBASE:
return self.get_preferred_runway(departure.airport).runway_heading
elif departure.is_fleet:
# The carrier will be angled into the wind automatically.
return (self.conditions.weather.wind.at_0m.direction + 180) % 360
logging.warning(
f"Unhandled departure control point: {departure.cptype}")
return 0
def landing_heading(self, arrival: ControlPoint) -> int:
if arrival.cptype == ControlPointType.AIRBASE:
return self.get_preferred_runway(arrival.airport).runway_heading
elif arrival.is_fleet:
# The carrier will be angled into the wind automatically.
return (self.conditions.weather.wind.at_0m.direction + 180) % 360
logging.warning(
f"Unhandled departure control point: {arrival.cptype}")
return 0

View File

@ -1,44 +1,48 @@
from typing import List
from pathlib import Path
from PySide2.QtCore import QSize, Qt, QItemSelectionModel, QPoint
from PySide2.QtWidgets import QLabel, QDialog, QGridLayout, QListView, QStackedLayout, QComboBox, QWidget, \
QAbstractItemView, QPushButton, QGroupBox, QCheckBox, QVBoxLayout, QSpinBox
import json import json
from pathlib import Path
from typing import List, Optional
class LuaPluginWorkOrder(): from PySide2.QtCore import Qt
from PySide2.QtWidgets import QCheckBox, QGridLayout, QGroupBox, QLabel
def __init__(self, parent, filename:str, mnemonic:str, disable:bool):
class LuaPluginWorkOrder:
def __init__(self, parent, filename: str, mnemonic: str,
disable: bool) -> None:
self.filename = filename self.filename = filename
self.mnemonic = mnemonic self.mnemonic = mnemonic
self.disable = disable self.disable = disable
self.parent = parent self.parent = parent
def work(self, mnemonic:str, operation): def work(self, operation):
if self.disable: if self.disable:
operation.bypassPluginScript(self.parent.mnemonic, self.mnemonic) operation.bypass_plugin_script(self.mnemonic)
else: else:
operation.injectPluginScript(self.parent.mnemonic, self.filename, self.mnemonic) operation.inject_plugin_script(self.parent.mnemonic, self.filename,
self.mnemonic)
class LuaPluginSpecificOption(): class LuaPluginSpecificOption:
def __init__(self, parent, mnemonic:str, nameInUI:str, defaultValue:bool): def __init__(self, parent, mnemonic: str, nameInUI: str,
defaultValue: bool) -> None:
self.mnemonic = mnemonic self.mnemonic = mnemonic
self.nameInUI = nameInUI self.nameInUI = nameInUI
self.defaultValue = defaultValue self.defaultValue = defaultValue
self.parent = parent self.parent = parent
class LuaPlugin(): class LuaPlugin:
NAME_IN_SETTINGS_BASE:str = "plugins." NAME_IN_SETTINGS_BASE:str = "plugins."
def __init__(self, jsonFilename:str): def __init__(self, jsonFilename: str) -> None:
self.mnemonic:str = None self.mnemonic: Optional[str] = None
self.skipUI:bool = False self.skipUI: bool = False
self.nameInUI:str = None self.nameInUI: Optional[str] = None
self.nameInSettings:str = None self.nameInSettings: Optional[str] = None
self.defaultValue:bool = False self.defaultValue: bool = False
self.specificOptions = [] self.specificOptions: List[LuaPluginSpecificOption] = []
self.scriptsWorkOrders: List[LuaPluginWorkOrder] = None self.scriptsWorkOrders: List[LuaPluginWorkOrder] = []
self.configurationWorkOrders: List[LuaPluginWorkOrder] = None self.configurationWorkOrders: List[LuaPluginWorkOrder] = []
self.initFromJson(jsonFilename) self.initFromJson(jsonFilename)
self.enabled = self.defaultValue self.enabled = self.defaultValue
self.settings = None self.settings = None
@ -50,6 +54,7 @@ class LuaPlugin():
self.mnemonic = jsonData.get("mnemonic") self.mnemonic = jsonData.get("mnemonic")
self.skipUI = jsonData.get("skipUI", False) self.skipUI = jsonData.get("skipUI", False)
self.nameInUI = jsonData.get("nameInUI") self.nameInUI = jsonData.get("nameInUI")
assert self.mnemonic is not None
self.nameInSettings = LuaPlugin.NAME_IN_SETTINGS_BASE + self.mnemonic self.nameInSettings = LuaPlugin.NAME_IN_SETTINGS_BASE + self.mnemonic
self.defaultValue = jsonData.get("defaultValue", False) self.defaultValue = jsonData.get("defaultValue", False)
self.specificOptions = [] self.specificOptions = []
@ -76,6 +81,9 @@ class LuaPlugin():
self.setSettings(settingsWindow.game.settings) self.setSettings(settingsWindow.game.settings)
if not self.skipUI: if not self.skipUI:
assert self.nameInSettings is not None
assert self.settings is not None
# create the plugin choice checkbox interface # create the plugin choice checkbox interface
self.uiWidget: QCheckBox = QCheckBox() self.uiWidget: QCheckBox = QCheckBox()
self.uiWidget.setChecked(self.isEnabled()) self.uiWidget.setChecked(self.isEnabled())
@ -95,6 +103,7 @@ class LuaPlugin():
# browse each option in the specific options list # browse each option in the specific options list
row = 0 row = 0
for specificOption in self.specificOptions: for specificOption in self.specificOptions:
assert specificOption.mnemonic is not None
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
if not nameInSettings in self.settings.plugins: if not nameInSettings in self.settings.plugins:
self.settings.plugins[nameInSettings] = specificOption.defaultValue self.settings.plugins[nameInSettings] = specificOption.defaultValue
@ -149,7 +158,7 @@ class LuaPlugin():
# execute the work order # execute the work order
if self.scriptsWorkOrders != None: if self.scriptsWorkOrders != None:
for workOrder in self.scriptsWorkOrders: for workOrder in self.scriptsWorkOrders:
workOrder.work(self.mnemonic, operation) workOrder.work(operation)
# serves for subclasses # serves for subclasses
return self.isEnabled() return self.isEnabled()
@ -177,12 +186,12 @@ class LuaPlugin():
lua += defineAllOptions lua += defineAllOptions
lua += "end" lua += "end"
operation.injectLuaTrigger(lua, f"{self.mnemonic} plugin configuration") operation.inject_lua_trigger(lua, f"{self.mnemonic} plugin configuration")
# execute the work order # execute the work order
if self.configurationWorkOrders != None: if self.configurationWorkOrders != None:
for workOrder in self.configurationWorkOrders: for workOrder in self.configurationWorkOrders:
workOrder.work(self.mnemonic, operation) workOrder.work(operation)
# serves for subclasses # serves for subclasses
return self.isEnabled() return self.isEnabled()

View File

@ -344,7 +344,7 @@ class QLiberationMap(QGraphicsView):
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL" altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
prefix = "TOT" prefix = "TOT"
time = timing.tot_for_waypoint(waypoint) time = timing.tot_for_waypoint(flight, waypoint)
if time is None: if time is None:
prefix = "Depart" prefix = "Depart"
time = timing.depart_time_for_waypoint(waypoint, flight) time = timing.depart_time_for_waypoint(waypoint, flight)

View File

@ -19,16 +19,12 @@ class QAirfieldCommand(QFrame):
layout = QGridLayout() layout = QGridLayout()
layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0) layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0)
try: planned = QGroupBox("Planned Flights")
planned = QGroupBox("Planned Flights") planned_layout = QVBoxLayout()
planned_layout = QVBoxLayout() planned_layout.addWidget(
planned_layout.addWidget( QPlannedFlightsView(self.game_model, self.cp)
QPlannedFlightsView(self.game_model, self.cp) )
) planned.setLayout(planned_layout)
planned.setLayout(planned_layout) layout.addWidget(planned, 0, 1)
layout.addWidget(planned, 0, 1)
except:
pass
#layout.addWidget(QAirportInformation(self.cp, self.game.theater.terrain.airport_by_id(self.cp.id)), 0, 2)
self.setLayout(layout) self.setLayout(layout)

View File

@ -25,7 +25,7 @@ class QPlannedFlightsView(QListView):
for package in self.game_model.ato_model.packages: for package in self.game_model.ato_model.packages:
for flight in package.flights: for flight in package.flights:
if flight.from_cp == self.cp: if flight.from_cp == self.cp:
item = QFlightItem(flight) item = QFlightItem(package.package, flight)
self.model.appendRow(item) self.model.appendRow(item)
self.flight_items.append(item) self.flight_items.append(item)
self.set_selected_flight(0) self.set_selected_flight(0)

View File

@ -107,7 +107,7 @@ class QFlightCreator(QDialog):
start_type = "Cold" start_type = "Cold"
else: else:
start_type = "Warm" start_type = "Warm"
flight = Flight(aircraft, size, origin, task, start_type) flight = Flight(self.package, aircraft, size, origin, task, start_type)
flight.scheduled_in = self.package.delay flight.scheduled_in = self.package.delay
flight.client_count = self.client_slots_spinner.value() flight.client_count = self.client_slots_spinner.value()

View File

@ -55,11 +55,12 @@ class QFlightWaypointList(QTableView):
waypoints = itertools.chain([takeoff], self.flight.points) waypoints = itertools.chain([takeoff], self.flight.points)
for row, waypoint in enumerate(waypoints): for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, waypoint, timing) self.add_waypoint_row(row, self.flight, waypoint, timing)
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)), self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),
QItemSelectionModel.Select) QItemSelectionModel.Select)
def add_waypoint_row(self, row: int, waypoint: FlightWaypoint, def add_waypoint_row(self, row: int, flight: Flight,
waypoint: FlightWaypoint,
timing: PackageWaypointTiming) -> None: timing: PackageWaypointTiming) -> None:
self.model.insertRow(self.model.rowCount()) self.model.insertRow(self.model.rowCount())
@ -71,15 +72,15 @@ class QFlightWaypointList(QTableView):
altitude_item.setEditable(False) altitude_item.setEditable(False)
self.model.setItem(row, 1, altitude_item) self.model.setItem(row, 1, altitude_item)
tot = self.tot_text(waypoint, timing) tot = self.tot_text(flight, waypoint, timing)
tot_item = QStandardItem(tot) tot_item = QStandardItem(tot)
tot_item.setEditable(False) tot_item.setEditable(False)
self.model.setItem(row, 2, tot_item) self.model.setItem(row, 2, tot_item)
def tot_text(self, waypoint: FlightWaypoint, def tot_text(self, flight: Flight, waypoint: FlightWaypoint,
timing: PackageWaypointTiming) -> str: timing: PackageWaypointTiming) -> str:
prefix = "" prefix = ""
time = timing.tot_for_waypoint(waypoint) time = timing.tot_for_waypoint(flight, waypoint)
if time is None: if time is None:
prefix = "Depart " prefix = "Depart "
time = timing.depart_time_for_waypoint(waypoint, self.flight) time = timing.depart_time_for_waypoint(waypoint, self.flight)

View File

@ -1,7 +1,9 @@
#pydcs>=0.9.10
Pyside2>=5.13.0 Pyside2>=5.13.0
pyinstaller==3.6 pyinstaller==3.6
pyproj==2.6.1.post1 pyproj==2.6.1.post1
Pillow~=7.2.0 Pillow~=7.2.0
tabulate~=0.8.7 tabulate~=0.8.7
mypy==0.782
mypy-extensions==0.4.3

View File

@ -1,5 +1,4 @@
[ [
"veaf",
"jtacautolase", "jtacautolase",
"base" "base"
] ]

View File

@ -2,7 +2,7 @@ import pickle
from typing import Collection, Optional, Tuple from typing import Collection, Optional, Tuple
Zone = Collection[Tuple[float, float]] Zone = Collection[Tuple[float, float]]
Landmap = Tuple[Collection[Zone], Collection[Zone]] Landmap = Tuple[Collection[Zone], Collection[Zone], Collection[Zone]]
def load_landmap(filename: str) -> Optional[Landmap]: def load_landmap(filename: str) -> Optional[Landmap]: