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
run: |
./venv/scripts/activate
mypy game
mypy gen
mypy theater
$env:PYTHONPATH=".;./pydcs"
pyinstaller pyinstaller.spec

View File

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

View File

@ -43,6 +43,7 @@ from dcs.planes import (
FA_18C_hornet,
FW_190A8,
FW_190D9,
F_117A,
F_14B,
F_15C,
F_15E,
@ -97,6 +98,9 @@ from dcs.planes import (
Su_34,
Tornado_GR4,
Tornado_IDS,
Tu_160,
Tu_22M3,
Tu_95MS,
WingLoong_I,
Yak_40,
plane_map,
@ -304,6 +308,10 @@ PRICES = {
# Bombers
B_52H: 35,
B_1B: 50,
F_117A: 100,
Tu_160: 50,
Tu_22M3: 40,
Tu_95MS: 35,
# special
IL_76MD: 30,
@ -584,49 +592,53 @@ UNIT_BY_TASK = {
SA342Mistral
],
CAS: [
F_15E,
F_86F_Sabre,
MiG_15bis,
L_39ZA,
AV8BNA,
AH_1W,
AH_64A,
AH_64D,
AJS37,
AV8BNA,
A_10A,
A_10C,
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,
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_30bl1,
P_47D_40,
Ju_88A4,
B_17G,
MB_339PAN,
Rafale_A_S,
WingLoong_I,
MQ_9_Reaper,
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: [
IL_76MD,
@ -942,6 +954,23 @@ COMMON_OVERRIDE = {
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: {
CAP: "CAP HEAVY",
Intercept: "CAP HEAVY",
@ -962,6 +991,15 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
GroundAttack: "STRIKE",
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_10C: 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_14B: COMMON_OVERRIDE,
F_15C: COMMON_OVERRIDE,
F_15E: COMMON_OVERRIDE,
F_16C_50: COMMON_OVERRIDE,
JF_17: 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 (
Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov,
@ -19,6 +27,8 @@ Russia_1955 = {
An_30M,
Yak_40,
Tu_95MS,
AirDefence.AAA_ZU_23_Closed,
AirDefence.AAA_ZU_23_on_Ural_375,
Armor.ARV_BRDM_2,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,11 +3,11 @@
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.
"""
from dataclasses import dataclass, field
import logging
from typing import Dict, Iterator, Optional, Tuple
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, Optional, Tuple
from dcs.terrain.terrain import Airport
from .radios import MHz, RadioFrequency
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)),
),
}
@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 os
import random
from collections import defaultdict
from dataclasses import dataclass
import random
from typing import List
from game import db
from dcs.mission import Mission
from game import db
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
from .runways import RunwayData
@dataclass

View File

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

View File

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

View File

@ -1,5 +1,7 @@
from __future__ import annotations
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.point import MovingPoint, PointAction
@ -8,6 +10,9 @@ from dcs.unittype import UnitType
from game import db
from theater.controlpoint import ControlPoint, MissionTarget
if TYPE_CHECKING:
from gen.ato import Package
class FlightType(Enum):
CAP = 0 # Do not use. Use BARCAP or TARCAP.
@ -138,10 +143,11 @@ class Flight:
use_custom_loadout = False
preset_loadout_name = ""
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,
flight_type: FlightType, start_type: str) -> None:
def __init__(self, package: Package, unit_type: UnitType, count: int,
from_cp: ControlPoint, flight_type: FlightType,
start_type: str) -> None:
self.package = package
self.unit_type = unit_type
self.count = count
self.from_cp = from_cp

View File

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

View File

@ -27,21 +27,22 @@ INGRESS_TYPES = {
FlightWaypointType.INGRESS_STRIKE,
}
IP_TYPES = {
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
FlightWaypointType.PATROL_TRACK,
}
class GroundSpeed:
@staticmethod
def mission_speed(package: Package) -> int:
speeds = set()
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:
logging.error(f"Could not find ingress point for {flight}.")
if flight.points:
@ -152,8 +153,10 @@ class TotEstimator:
# Takeoff immediately.
return 0
if self.package.primary_task == FlightType.BARCAP:
start_time = self.timing.race_track_start
# BARCAP flights do not coordinate with the rest of the package on join
# or ingress points.
if flight.flight_type == FlightType.BARCAP:
start_time = self.timing.race_track_start(flight)
else:
start_time = self.timing.join
return start_time - travel_time - self.HOLD_TIME
@ -166,7 +169,9 @@ class TotEstimator:
def earliest_tot_for_flight(self, flight: Flight) -> int:
"""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.
@ -177,27 +182,34 @@ class TotEstimator:
The earliest possible TOT for the given flight in seconds. Returns 0
if an ingress point cannot be found.
"""
time_to_ingress = self.estimate_waypoints_to_target(flight, IP_TYPES)
if flight.flight_type == FlightType.BARCAP:
time_to_target = self.estimate_waypoints_to_target(flight, {
FlightWaypointType.PATROL_TRACK
})
if time_to_target is None:
logging.warning(
f"Found no race track. Cannot estimate TOT for {flight}")
# Return 0 so this flight's travel time does not affect the rest
# of the package.
return 0
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 so this flight's travel time does not affect the rest
# of the package.
return 0
if self.package.primary_task == FlightType.BARCAP:
# The racetrack start *is* the target. The package target is the
# protected objective.
time_to_target = 0
else:
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,
GroundSpeed.mission_speed(self.package))
return sum([
self.estimate_startup(flight),
self.estimate_ground_ops(flight),
time_to_ingress,
time_to_target,
])
@ -281,18 +293,22 @@ class PackageWaypointTiming:
assert self.package.time_over_target is not None
return self.package.time_over_target
@property
def race_track_start(self) -> int:
if self.package.primary_task == FlightType.BARCAP:
return self.package.time_over_target
def race_track_start(self, flight: Flight) -> int:
if flight.flight_type == FlightType.BARCAP:
return self.target
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
@property
def race_track_end(self) -> int:
if self.package.primary_task == FlightType.BARCAP:
def race_track_end(self, flight: Flight) -> int:
if flight.flight_type == FlightType.BARCAP:
return self.target + CAP_DURATION * 60
else:
# For TARCAP. See the explanation in race_track_start.
return self.egress
def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int:
@ -303,7 +319,8 @@ class PackageWaypointTiming:
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 = (
FlightWaypointType.TARGET_GROUP_LOC,
FlightWaypointType.TARGET_POINT,
@ -321,7 +338,7 @@ class PackageWaypointTiming:
elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
return self.split
elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK:
return self.race_track_start
return self.race_track_start(flight)
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
@ -329,7 +346,7 @@ class PackageWaypointTiming:
if waypoint.waypoint_type == FlightWaypointType.LOITER:
return self.push_time(flight, waypoint)
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
return self.race_track_end
return self.race_track_end(flight)
return None
@classmethod

View File

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

View File

@ -16,9 +16,9 @@ from dcs.unitgroup import StaticGroup
from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name
from .airfields import RunwayData
from .conflictgen import Conflict
from .radios import RadioRegistry
from .runways import RunwayData
from .tacan import TacanBand, TacanRegistry
FARP_FRONTLINE_DISTANCE = 10000
@ -141,8 +141,9 @@ class GroundObjectsGenerator:
# Find carrier direction (In the wind)
found_carrier_destination = False
attempt = 0
brc = self.m.weather.wind_at_ground.direction + 180
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):
found_carrier_destination = True
sg.add_waypoint(point)
@ -196,6 +197,7 @@ class GroundObjectsGenerator:
# unit name since it's an arbitrary ID.
self.runways[cp.name] = RunwayData(
cp.name,
brc,
"N/A",
atc=atc_channel,
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
aircraft will be able to see the enemy's kneeboard for the same airframe.
"""
import datetime
from collections import defaultdict
from dataclasses import dataclass
import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from PIL import Image, ImageDraw, ImageFont
from dcs.mapping import Point
from dcs.mission import Mission
from dcs.unittype import FlyingType
from tabulate import tabulate
@ -37,12 +36,11 @@ from tabulate import tabulate
from game.utils import meter_to_nm
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 .flights.traveltime import TravelTime
from .radios import RadioFrequency
from .runways import RunwayData
class KneeboardPageWriter:
@ -126,6 +124,7 @@ class FlightPlanBuilder:
self.target_points = []
self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint))
self.last_waypoint = waypoint
def coalesce_target_points(self) -> None:
if len(self.target_points) <= 4:
@ -157,7 +156,6 @@ class FlightPlanBuilder:
self._format_time(waypoint.waypoint.tot),
self._format_time(waypoint.waypoint.departure_time),
])
self.last_waypoint = waypoint.waypoint
def _format_time(self, time: Optional[int]) -> str:
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
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.mnemonic = mnemonic
self.disable = disable
self.parent = parent
def work(self, mnemonic:str, operation):
def work(self, operation):
if self.disable:
operation.bypassPluginScript(self.parent.mnemonic, self.mnemonic)
operation.bypass_plugin_script(self.mnemonic)
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.nameInUI = nameInUI
self.defaultValue = defaultValue
self.parent = parent
class LuaPlugin():
class LuaPlugin:
NAME_IN_SETTINGS_BASE:str = "plugins."
def __init__(self, jsonFilename:str):
self.mnemonic:str = None
def __init__(self, jsonFilename: str) -> None:
self.mnemonic: Optional[str] = None
self.skipUI: bool = False
self.nameInUI:str = None
self.nameInSettings:str = None
self.nameInUI: Optional[str] = None
self.nameInSettings: Optional[str] = None
self.defaultValue: bool = False
self.specificOptions = []
self.scriptsWorkOrders: List[LuaPluginWorkOrder] = None
self.configurationWorkOrders: List[LuaPluginWorkOrder] = None
self.specificOptions: List[LuaPluginSpecificOption] = []
self.scriptsWorkOrders: List[LuaPluginWorkOrder] = []
self.configurationWorkOrders: List[LuaPluginWorkOrder] = []
self.initFromJson(jsonFilename)
self.enabled = self.defaultValue
self.settings = None
@ -50,6 +54,7 @@ class LuaPlugin():
self.mnemonic = jsonData.get("mnemonic")
self.skipUI = jsonData.get("skipUI", False)
self.nameInUI = jsonData.get("nameInUI")
assert self.mnemonic is not None
self.nameInSettings = LuaPlugin.NAME_IN_SETTINGS_BASE + self.mnemonic
self.defaultValue = jsonData.get("defaultValue", False)
self.specificOptions = []
@ -76,6 +81,9 @@ class LuaPlugin():
self.setSettings(settingsWindow.game.settings)
if not self.skipUI:
assert self.nameInSettings is not None
assert self.settings is not None
# create the plugin choice checkbox interface
self.uiWidget: QCheckBox = QCheckBox()
self.uiWidget.setChecked(self.isEnabled())
@ -95,6 +103,7 @@ class LuaPlugin():
# browse each option in the specific options list
row = 0
for specificOption in self.specificOptions:
assert specificOption.mnemonic is not None
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
if not nameInSettings in self.settings.plugins:
self.settings.plugins[nameInSettings] = specificOption.defaultValue
@ -149,7 +158,7 @@ class LuaPlugin():
# execute the work order
if self.scriptsWorkOrders != None:
for workOrder in self.scriptsWorkOrders:
workOrder.work(self.mnemonic, operation)
workOrder.work(operation)
# serves for subclasses
return self.isEnabled()
@ -177,12 +186,12 @@ class LuaPlugin():
lua += defineAllOptions
lua += "end"
operation.injectLuaTrigger(lua, f"{self.mnemonic} plugin configuration")
operation.inject_lua_trigger(lua, f"{self.mnemonic} plugin configuration")
# execute the work order
if self.configurationWorkOrders != None:
for workOrder in self.configurationWorkOrders:
workOrder.work(self.mnemonic, operation)
workOrder.work(operation)
# serves for subclasses
return self.isEnabled()

View File

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

View File

@ -19,7 +19,6 @@ class QAirfieldCommand(QFrame):
layout = QGridLayout()
layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0)
try:
planned = QGroupBox("Planned Flights")
planned_layout = QVBoxLayout()
planned_layout.addWidget(
@ -27,8 +26,5 @@ class QAirfieldCommand(QFrame):
)
planned.setLayout(planned_layout)
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)

View File

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

View File

@ -107,7 +107,7 @@ class QFlightCreator(QDialog):
start_type = "Cold"
else:
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.client_count = self.client_slots_spinner.value()

View File

@ -55,11 +55,12 @@ class QFlightWaypointList(QTableView):
waypoints = itertools.chain([takeoff], self.flight.points)
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)),
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:
self.model.insertRow(self.model.rowCount())
@ -71,15 +72,15 @@ class QFlightWaypointList(QTableView):
altitude_item.setEditable(False)
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.setEditable(False)
self.model.setItem(row, 2, tot_item)
def tot_text(self, waypoint: FlightWaypoint,
def tot_text(self, flight: Flight, waypoint: FlightWaypoint,
timing: PackageWaypointTiming) -> str:
prefix = ""
time = timing.tot_for_waypoint(waypoint)
time = timing.tot_for_waypoint(flight, waypoint)
if time is None:
prefix = "Depart "
time = timing.depart_time_for_waypoint(waypoint, self.flight)

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import pickle
from typing import Collection, Optional, Tuple
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]: