mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Finish moving gen into game.
This commit is contained in:
179
game/airfields.py
Normal file
179
game/airfields.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""Extra airfield data that is not exposed by pydcs.
|
||||
|
||||
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 __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar, Dict, Optional, TYPE_CHECKING, Tuple
|
||||
|
||||
import yaml
|
||||
from dcs.terrain import Airport
|
||||
|
||||
from game.radio.radios import RadioFrequency
|
||||
from game.radio.tacan import TacanChannel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
@dataclass
|
||||
class AtcData:
|
||||
hf: RadioFrequency
|
||||
vhf_fm: RadioFrequency
|
||||
vhf_am: RadioFrequency
|
||||
uhf: RadioFrequency
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, data: dict[str, Any]) -> Optional[AtcData]:
|
||||
atc_data = data.get("atc")
|
||||
if atc_data is None:
|
||||
return None
|
||||
return AtcData(
|
||||
RadioFrequency.parse(atc_data["hf"]),
|
||||
RadioFrequency.parse(atc_data["vhf_low"]),
|
||||
RadioFrequency.parse(atc_data["vhf_high"]),
|
||||
RadioFrequency.parse(atc_data["uhf"]),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirfieldData:
|
||||
"""Additional airfield data not included in pydcs."""
|
||||
|
||||
#: Airfield name for the UI. Not stable.
|
||||
name: str
|
||||
|
||||
#: pydcs airport ID
|
||||
id: int
|
||||
|
||||
#: ICAO airport code
|
||||
icao: Optional[str] = None
|
||||
|
||||
#: Elevation (in ft).
|
||||
elevation: int = 0
|
||||
|
||||
#: Runway length (in ft).
|
||||
runway_length: int = 0
|
||||
|
||||
#: TACAN channel for the airfield.
|
||||
tacan: Optional[TacanChannel] = None
|
||||
|
||||
#: TACAN callsign
|
||||
tacan_callsign: Optional[str] = None
|
||||
|
||||
#: VOR as a tuple of (callsign, frequency).
|
||||
vor: Optional[Tuple[str, RadioFrequency]] = None
|
||||
|
||||
#: RSBN channel as a tuple of (callsign, channel).
|
||||
rsbn: Optional[Tuple[str, int]] = None
|
||||
|
||||
#: Radio channels used by the airfield's ATC. Note that not all airfields
|
||||
#: have ATCs.
|
||||
atc: Optional[AtcData] = None
|
||||
|
||||
#: Dict of runway heading -> ILS tuple of (callsign, frequency).
|
||||
ils: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict)
|
||||
|
||||
#: Dict of runway heading -> PRMG tuple of (callsign, channel).
|
||||
prmg: Dict[str, Tuple[str, int]] = field(default_factory=dict)
|
||||
|
||||
#: Dict of runway heading -> outer NDB tuple of (callsign, frequency).
|
||||
outer_ndb: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict)
|
||||
|
||||
#: Dict of runway heading -> inner NDB tuple of (callsign, frequency).
|
||||
inner_ndb: Dict[str, Tuple[str, RadioFrequency]] = field(default_factory=dict)
|
||||
|
||||
_airfields: ClassVar[dict[str, dict[int, AirfieldData]]] = {}
|
||||
|
||||
def ils_freq(self, runway: str) -> Optional[RadioFrequency]:
|
||||
ils = self.ils.get(runway)
|
||||
if ils is not None:
|
||||
return ils[1]
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, airfield_yaml: Path) -> AirfieldData:
|
||||
with airfield_yaml.open() as yaml_file:
|
||||
data = yaml.safe_load(yaml_file)
|
||||
|
||||
tacan_channel = None
|
||||
tacan_callsign = None
|
||||
if (tacan := data.get("tacan")) is not None:
|
||||
tacan_channel = TacanChannel.parse(tacan["channel"])
|
||||
tacan_callsign = tacan["callsign"]
|
||||
|
||||
vor = None
|
||||
if (vor_data := data.get("vor")) is not None:
|
||||
vor = (vor_data["callsign"], RadioFrequency.parse(vor_data["frequency"]))
|
||||
|
||||
rsbn = None
|
||||
if (rsbn_data := data.get("rsbn")) is not None:
|
||||
rsbn = (rsbn_data["callsign"], rsbn_data["channel"])
|
||||
|
||||
ils = {}
|
||||
prmg = {}
|
||||
outer_ndb = {}
|
||||
inner_ndb = {}
|
||||
for name, runway_data in data.get("runways", {}).items():
|
||||
if (ils_data := runway_data.get("ils")) is not None:
|
||||
ils[name] = (
|
||||
ils_data["callsign"],
|
||||
RadioFrequency.parse(ils_data["frequency"]),
|
||||
)
|
||||
|
||||
if (prmg_data := runway_data.get("prmg")) is not None:
|
||||
prmg[name] = (prmg_data["callsign"], prmg_data["channel"])
|
||||
|
||||
if (outer_ndb_data := runway_data.get("outer_ndb")) is not None:
|
||||
outer_ndb[name] = (
|
||||
outer_ndb_data["callsign"],
|
||||
RadioFrequency.parse(outer_ndb_data["frequency"]),
|
||||
)
|
||||
|
||||
if (inner_ndb_data := runway_data.get("inner_ndb")) is not None:
|
||||
inner_ndb[name] = (
|
||||
inner_ndb_data["callsign"],
|
||||
RadioFrequency.parse(inner_ndb_data["frequency"]),
|
||||
)
|
||||
|
||||
return AirfieldData(
|
||||
data["name"],
|
||||
data["id"],
|
||||
data.get("icao"),
|
||||
data["elevation"],
|
||||
data["runway_length"],
|
||||
tacan_channel,
|
||||
tacan_callsign,
|
||||
vor,
|
||||
rsbn,
|
||||
AtcData.from_yaml(data),
|
||||
ils,
|
||||
prmg,
|
||||
outer_ndb,
|
||||
inner_ndb,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _load_for_theater_if_needed(cls, theater: ConflictTheater) -> None:
|
||||
if theater.terrain.name in cls._airfields:
|
||||
return
|
||||
|
||||
airfields = {}
|
||||
base_path = Path("resources/airfields") / theater.terrain.name
|
||||
for airfield_yaml in base_path.iterdir():
|
||||
data = cls.from_file(airfield_yaml)
|
||||
airfields[data.id] = data
|
||||
cls._airfields[theater.terrain.name] = airfields
|
||||
|
||||
@classmethod
|
||||
def for_airport(cls, theater: ConflictTheater, airport: Airport) -> AirfieldData:
|
||||
return cls._airfields[theater.terrain.name][airport.id]
|
||||
|
||||
@classmethod
|
||||
def for_theater(cls, theater: ConflictTheater) -> Iterator[AirfieldData]:
|
||||
cls._load_for_theater_if_needed(theater)
|
||||
yield from cls._airfields[theater.terrain.name].values()
|
||||
560
game/ato/ai_flight_planner_db.py
Normal file
560
game/ato/ai_flight_planner_db.py
Normal file
@@ -0,0 +1,560 @@
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from typing import Type
|
||||
|
||||
from dcs.helicopters import (
|
||||
AH_1W,
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
CH_47D,
|
||||
CH_53E,
|
||||
Ka_50,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Mi_26,
|
||||
Mi_28N,
|
||||
Mi_8MT,
|
||||
OH_58D,
|
||||
SA342L,
|
||||
SA342M,
|
||||
SH_60B,
|
||||
UH_1H,
|
||||
UH_60A,
|
||||
)
|
||||
from dcs.planes import (
|
||||
AJS37,
|
||||
AV8BNA,
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
A_20G,
|
||||
A_50,
|
||||
An_26B,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
C_130,
|
||||
C_17A,
|
||||
E_2C,
|
||||
E_3A,
|
||||
FA_18C_hornet,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_117A,
|
||||
F_14A,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
H_6J,
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
I_16,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Ju_88A4,
|
||||
KC130,
|
||||
KC135MPRS,
|
||||
KC_135,
|
||||
KJ_2000,
|
||||
L_39ZA,
|
||||
MQ_9_Reaper,
|
||||
M_2000C,
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29S,
|
||||
MiG_31,
|
||||
Mirage_2000_5,
|
||||
MosquitoFBMkVI,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
RQ_1A_Predator,
|
||||
S_3B,
|
||||
S_3B_Tanker,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_27,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_34,
|
||||
Tornado_GR4,
|
||||
Tornado_IDS,
|
||||
Tu_142,
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
Yak_40,
|
||||
)
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.f104.f104 import VSN_F104G, VSN_F104S, VSN_F104S_AG
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
from pydcs_extensions.uh60l.uh60l import KC130J, UH_60L
|
||||
from .flighttype import FlightType
|
||||
|
||||
# All aircraft lists are in priority order. Aircraft higher in the list will be
|
||||
# preferred over those lower in the list.
|
||||
# TODO: These lists really ought to be era (faction) dependent.
|
||||
# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
|
||||
# factions that also have F-4s should not.
|
||||
|
||||
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
|
||||
CAP_CAPABLE = [
|
||||
Su_57,
|
||||
F_22A,
|
||||
F_15C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
F_14A,
|
||||
Su_33,
|
||||
J_11A,
|
||||
Su_30,
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
JF_17,
|
||||
JAS39Gripen,
|
||||
F_16A,
|
||||
F_4E,
|
||||
MiG_31,
|
||||
MiG_25PD,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
MiG_23MLD,
|
||||
MiG_21Bis,
|
||||
Mirage_2000_5,
|
||||
F_15E,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
VSN_F104S,
|
||||
VSN_F104G,
|
||||
MiG_19P,
|
||||
A_4E_C,
|
||||
F_86F_Sabre,
|
||||
MiG_15bis,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
I_16,
|
||||
]
|
||||
|
||||
|
||||
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
|
||||
CAS_CAPABLE = [
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
Hercules,
|
||||
Su_34,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
F_15E,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
Tornado_GR4,
|
||||
Tornado_IDS,
|
||||
JAS39Gripen_AG,
|
||||
JF_17,
|
||||
AV8BNA,
|
||||
A_10A,
|
||||
B_1B,
|
||||
A_4E_C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
AJS37,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
Su_33,
|
||||
F_4E,
|
||||
S_3B,
|
||||
Su_30,
|
||||
MiG_29S,
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
MiG_21Bis,
|
||||
AH_64D,
|
||||
AH_64A,
|
||||
AH_1W,
|
||||
OH_58D,
|
||||
SA342M,
|
||||
SA342L,
|
||||
Ka_50,
|
||||
Mi_28N,
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Mi_8MT,
|
||||
H_6J,
|
||||
MiG_19P,
|
||||
MiG_15bis,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
UH_1H,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
I_16,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator,
|
||||
]
|
||||
|
||||
|
||||
# Aircraft used for SEAD and SEAD Escort tasks. Must be capable of the CAS DCS task.
|
||||
SEAD_CAPABLE = [
|
||||
JF_17,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
Tornado_IDS,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
F_4E,
|
||||
A_4E_C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
JAS39Gripen_AG,
|
||||
AV8BNA,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
Su_34,
|
||||
Su_30,
|
||||
MiG_27K,
|
||||
Tornado_GR4,
|
||||
]
|
||||
|
||||
|
||||
# Aircraft used for DEAD tasks. Must be capable of the CAS DCS task.
|
||||
DEAD_CAPABLE = [
|
||||
AJS37,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
JAS39Gripen_AG,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Tu_160,
|
||||
Tu_95MS,
|
||||
H_6J,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
] + SEAD_CAPABLE
|
||||
|
||||
|
||||
# Aircraft used for Strike mission
|
||||
STRIKE_CAPABLE = [
|
||||
F_117A,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Tu_160,
|
||||
Tu_95MS,
|
||||
Tu_22M3,
|
||||
H_6J,
|
||||
F_15E,
|
||||
AJS37,
|
||||
Tornado_GR4,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
JAS39Gripen_AG,
|
||||
Tornado_IDS,
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
Su_34,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
F_4E,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
M_2000C,
|
||||
MiG_27K,
|
||||
MiG_21Bis,
|
||||
MiG_15bis,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
B_17G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
]
|
||||
|
||||
|
||||
ANTISHIP_CAPABLE = [
|
||||
AJS37,
|
||||
Tu_142,
|
||||
Tu_22M3,
|
||||
H_6J,
|
||||
FA_18C_hornet,
|
||||
JAS39Gripen_AG,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
JF_17,
|
||||
Su_34,
|
||||
Su_30,
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
AV8BNA,
|
||||
S_3B,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
MosquitoFBMkVI,
|
||||
C_101CC,
|
||||
SH_60B,
|
||||
]
|
||||
|
||||
|
||||
# This list does not "inherit" from the strike list because some strike aircraft can
|
||||
# only carry guided weapons, and the AI cannot do runway attack with dguided weapons.
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/1703
|
||||
RUNWAY_ATTACK_CAPABLE = [
|
||||
JF_17,
|
||||
Su_34,
|
||||
Su_30,
|
||||
Tornado_IDS,
|
||||
M_2000C,
|
||||
H_6J,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Tu_22M3,
|
||||
H_6J,
|
||||
F_15E,
|
||||
AJS37,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
JAS39Gripen_AG,
|
||||
Tornado_IDS,
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
Su_34,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
F_4E,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
M_2000C,
|
||||
MiG_27K,
|
||||
MiG_21Bis,
|
||||
MiG_15bis,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
B_17G,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
MosquitoFBMkVI,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
]
|
||||
|
||||
# For any aircraft that isn't necessarily directly involved in strike
|
||||
# missions in a direct combat sense, but can transport objects and infantry.
|
||||
TRANSPORT_CAPABLE = [
|
||||
C_17A,
|
||||
Hercules,
|
||||
C_130,
|
||||
IL_76MD,
|
||||
An_26B,
|
||||
Yak_40,
|
||||
CH_53E,
|
||||
CH_47D,
|
||||
UH_60L,
|
||||
SH_60B,
|
||||
UH_60A,
|
||||
UH_1H,
|
||||
Mi_8MT,
|
||||
Mi_8MT,
|
||||
Mi_26,
|
||||
]
|
||||
|
||||
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
|
||||
|
||||
AEWC_CAPABLE = [
|
||||
E_3A,
|
||||
E_2C,
|
||||
A_50,
|
||||
KJ_2000,
|
||||
]
|
||||
|
||||
# Priority is given to the tankers that can carry the most fuel.
|
||||
REFUELING_CAPABALE = [
|
||||
KC_135,
|
||||
KC135MPRS,
|
||||
IL_78M,
|
||||
KC130J,
|
||||
KC130,
|
||||
S_3B_Tanker,
|
||||
]
|
||||
|
||||
|
||||
def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
|
||||
cap_missions = (
|
||||
FlightType.BARCAP,
|
||||
FlightType.INTERCEPTION,
|
||||
FlightType.SWEEP,
|
||||
FlightType.TARCAP,
|
||||
)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.ANTISHIP:
|
||||
return ANTISHIP_CAPABLE
|
||||
elif task == FlightType.BAI:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.SEAD:
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.SEAD_ESCORT:
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.DEAD:
|
||||
return DEAD_CAPABLE
|
||||
elif task == FlightType.OCA_AIRCRAFT:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.OCA_RUNWAY:
|
||||
return RUNWAY_ATTACK_CAPABLE
|
||||
elif task == FlightType.STRIKE:
|
||||
return STRIKE_CAPABLE
|
||||
elif task == FlightType.ESCORT:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.AEWC:
|
||||
return AEWC_CAPABLE
|
||||
elif task == FlightType.REFUELING:
|
||||
return REFUELING_CAPABALE
|
||||
elif task == FlightType.TRANSPORT:
|
||||
return TRANSPORT_CAPABLE
|
||||
else:
|
||||
logging.error(f"Unplannable flight type: {task}")
|
||||
return []
|
||||
|
||||
|
||||
def aircraft_for_task(task: FlightType) -> list[AircraftType]:
|
||||
dcs_types = dcs_types_for_task(task)
|
||||
types: list[AircraftType] = []
|
||||
for dcs_type in dcs_types:
|
||||
types.extend(AircraftType.for_dcs_type(dcs_type))
|
||||
return types
|
||||
|
||||
|
||||
def tasks_for_aircraft(aircraft: AircraftType) -> list[FlightType]:
|
||||
tasks: list[FlightType] = []
|
||||
for task in FlightType:
|
||||
if task is FlightType.FERRY:
|
||||
# Not a plannable task, so skip it.
|
||||
continue
|
||||
if aircraft in aircraft_for_task(task):
|
||||
tasks.append(task)
|
||||
return tasks
|
||||
80
game/ato/closestairfields.py
Normal file
80
game/ato/closestairfields.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Objective adjacency lists."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
|
||||
|
||||
from game.utils import Distance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater, ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class ClosestAirfields:
|
||||
"""Precalculates which control points are closes to the given target."""
|
||||
|
||||
def __init__(
|
||||
self, target: MissionTarget, all_control_points: List[ControlPoint]
|
||||
) -> None:
|
||||
self.target = target
|
||||
# This cache is configured once on load, so it's important that it is
|
||||
# complete and deterministic to avoid different behaviors across loads.
|
||||
# E.g. https://github.com/dcs-liberation/dcs_liberation/issues/819
|
||||
self.closest_airfields: List[ControlPoint] = sorted(
|
||||
all_control_points, key=lambda c: self.target.distance_to(c)
|
||||
)
|
||||
|
||||
@property
|
||||
def operational_airfields(self) -> Iterator[ControlPoint]:
|
||||
return (c for c in self.closest_airfields if c.runway_is_operational())
|
||||
|
||||
def _airfields_within(
|
||||
self, distance: Distance, operational: bool
|
||||
) -> Iterator[ControlPoint]:
|
||||
airfields = (
|
||||
self.operational_airfields if operational else self.closest_airfields
|
||||
)
|
||||
for cp in airfields:
|
||||
if cp.distance_to(self.target) < distance.meters:
|
||||
yield cp
|
||||
else:
|
||||
break
|
||||
|
||||
def operational_airfields_within(
|
||||
self, distance: Distance
|
||||
) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
return self._airfields_within(distance, operational=True)
|
||||
|
||||
def all_airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
return self._airfields_within(distance, operational=False)
|
||||
|
||||
|
||||
class ObjectiveDistanceCache:
|
||||
theater: Optional[ConflictTheater] = None
|
||||
closest_airfields: Dict[str, ClosestAirfields] = {}
|
||||
|
||||
@classmethod
|
||||
def set_theater(cls, theater: ConflictTheater) -> None:
|
||||
if cls.theater is not None:
|
||||
cls.closest_airfields = {}
|
||||
cls.theater = theater
|
||||
|
||||
@classmethod
|
||||
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
|
||||
if cls.theater is None:
|
||||
raise RuntimeError("Call ObjectiveDistanceCache.set_theater before using")
|
||||
|
||||
if location.name not in cls.closest_airfields:
|
||||
cls.closest_airfields[location.name] = ClosestAirfields(
|
||||
location, cls.theater.controlpoints
|
||||
)
|
||||
return cls.closest_airfields[location.name]
|
||||
@@ -7,10 +7,10 @@ from typing import Any, List, Optional, TYPE_CHECKING
|
||||
from dcs import Point
|
||||
from dcs.planes import C_101CC, C_101EB, Su_33
|
||||
|
||||
from gen.flights.loadouts import Loadout
|
||||
from game.savecompat import has_save_compat_for
|
||||
from .flightroster import FlightRoster
|
||||
from .flightstate import FlightState, Uninitialized
|
||||
from ..savecompat import has_save_compat_for
|
||||
from .loadouts import Loadout
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
@@ -74,7 +74,7 @@ class Flight:
|
||||
# Will be replaced with a more appropriate FlightPlan by
|
||||
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
||||
# empty flight plan.
|
||||
from gen.flights.flightplan import FlightPlan, CustomFlightPlan
|
||||
from game.ato.flightplan import FlightPlan, CustomFlightPlan
|
||||
|
||||
self.flight_plan: FlightPlan = CustomFlightPlan(
|
||||
package=package, flight=self, custom_waypoints=[]
|
||||
|
||||
2133
game/ato/flightplan.py
Normal file
2133
game/ato/flightplan.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from game.ato.traveltime import TotEstimator
|
||||
from .flightstate import FlightState
|
||||
from ..starttype import StartType
|
||||
|
||||
|
||||
190
game/ato/loadouts.py
Normal file
190
game/ato/loadouts.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from collections.abc import Iterable
|
||||
from typing import Iterator, Mapping, Optional, TYPE_CHECKING
|
||||
|
||||
from game.data.weapons import Pylon, Weapon, WeaponType
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .flight import Flight
|
||||
|
||||
|
||||
class Loadout:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
pylons: Mapping[int, Optional[Weapon]],
|
||||
date: Optional[datetime.date],
|
||||
is_custom: bool = False,
|
||||
) -> None:
|
||||
self.name = name
|
||||
# We clear unused pylon entries on initialization, but UI actions can still
|
||||
# cause a pylon to be emptied, so make the optional type explicit.
|
||||
self.pylons: Mapping[int, Optional[Weapon]] = {
|
||||
k: v for k, v in pylons.items() if v is not None
|
||||
}
|
||||
self.date = date
|
||||
self.is_custom = is_custom
|
||||
|
||||
def derive_custom(self, name: str) -> Loadout:
|
||||
return Loadout(name, self.pylons, self.date, is_custom=True)
|
||||
|
||||
def has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
|
||||
for weapon in self.pylons.values():
|
||||
if weapon is not None and weapon.weapon_group.type is weapon_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _fallback_for(
|
||||
weapon: Weapon,
|
||||
pylon: Pylon,
|
||||
date: datetime.date,
|
||||
skip_types: Optional[Iterable[WeaponType]] = None,
|
||||
) -> Optional[Weapon]:
|
||||
if skip_types is None:
|
||||
skip_types = set()
|
||||
for fallback in weapon.fallbacks:
|
||||
if not pylon.can_equip(fallback):
|
||||
continue
|
||||
if not fallback.available_on(date):
|
||||
continue
|
||||
if fallback.weapon_group.type in skip_types:
|
||||
continue
|
||||
return fallback
|
||||
return None
|
||||
|
||||
def degrade_for_date(self, unit_type: AircraftType, date: datetime.date) -> Loadout:
|
||||
if self.date is not None and self.date <= date:
|
||||
return Loadout(self.name, self.pylons, self.date, self.is_custom)
|
||||
|
||||
new_pylons = dict(self.pylons)
|
||||
for pylon_number, weapon in self.pylons.items():
|
||||
if weapon is None:
|
||||
del new_pylons[pylon_number]
|
||||
continue
|
||||
if not weapon.available_on(date):
|
||||
pylon = Pylon.for_aircraft(unit_type, pylon_number)
|
||||
fallback = self._fallback_for(weapon, pylon, date)
|
||||
if fallback is None:
|
||||
del new_pylons[pylon_number]
|
||||
else:
|
||||
new_pylons[pylon_number] = fallback
|
||||
loadout = Loadout(self.name, new_pylons, date, self.is_custom)
|
||||
# If this is not a custom loadout, we should replace any LGBs with iron bombs if
|
||||
# the loadout lost its TGP.
|
||||
#
|
||||
# If the loadout was chosen explicitly by the user, assume they know what
|
||||
# they're doing. They may be coordinating buddy-lase.
|
||||
if not loadout.is_custom:
|
||||
loadout.replace_lgbs_if_no_tgp(unit_type, date)
|
||||
return loadout
|
||||
|
||||
def replace_lgbs_if_no_tgp(
|
||||
self, unit_type: AircraftType, date: datetime.date
|
||||
) -> None:
|
||||
if self.has_weapon_of_type(WeaponType.TGP):
|
||||
return
|
||||
|
||||
new_pylons = dict(self.pylons)
|
||||
for pylon_number, weapon in self.pylons.items():
|
||||
if weapon is not None and weapon.weapon_group.type is WeaponType.LGB:
|
||||
pylon = Pylon.for_aircraft(unit_type, pylon_number)
|
||||
fallback = self._fallback_for(
|
||||
weapon, pylon, date, skip_types={WeaponType.LGB}
|
||||
)
|
||||
if fallback is None:
|
||||
del new_pylons[pylon_number]
|
||||
else:
|
||||
new_pylons[pylon_number] = fallback
|
||||
self.pylons = new_pylons
|
||||
|
||||
@classmethod
|
||||
def iter_for(cls, flight: Flight) -> Iterator[Loadout]:
|
||||
# Dict of payload ID (numeric) to:
|
||||
#
|
||||
# {
|
||||
# "name": The name the user set in the ME
|
||||
# "pylons": List (as a dict) of dicts of:
|
||||
# {"CLSID": class ID, "num": pylon number}
|
||||
# "tasks": List (as a dict) of task IDs the payload is used by.
|
||||
# }
|
||||
payloads = flight.unit_type.dcs_unit_type.load_payloads()
|
||||
for payload in payloads.values():
|
||||
name = payload["name"]
|
||||
pylons = payload["pylons"]
|
||||
yield Loadout(
|
||||
name,
|
||||
{p["num"]: Weapon.with_clsid(p["CLSID"]) for p in pylons.values()},
|
||||
date=None,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def default_loadout_names_for(cls, flight: Flight) -> Iterator[str]:
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
# This is a list of mappings from the FlightType of a Flight to the type of
|
||||
# payload defined in the resources/payloads/UNIT_TYPE.lua file. A Flight has no
|
||||
# concept of a PyDCS task, so COMMON_OVERRIDE cannot be used here. This is used
|
||||
# in the payload editor, for setting the default loadout of an object. The left
|
||||
# element is the FlightType name, and the right element is a tuple containing
|
||||
# what is used in the lua file. Some aircraft differ from the standard loadout
|
||||
# names, so those have been included here too. The priority goes from first to
|
||||
# last - the first element in the tuple will be tried first, then the second,
|
||||
# etc.
|
||||
loadout_names = {t: [f"Liberation {t.value}"] for t in FlightType}
|
||||
legacy_names = {
|
||||
FlightType.TARCAP: ("CAP HEAVY", "CAP", "Liberation BARCAP"),
|
||||
FlightType.BARCAP: ("CAP HEAVY", "CAP", "Liberation TARCAP"),
|
||||
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
|
||||
FlightType.STRIKE: ("STRIKE",),
|
||||
FlightType.ANTISHIP: ("ANTISHIP",),
|
||||
FlightType.DEAD: ("DEAD",),
|
||||
FlightType.SEAD: ("SEAD",),
|
||||
FlightType.BAI: ("BAI",),
|
||||
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"),
|
||||
FlightType.OCA_AIRCRAFT: ("OCA",),
|
||||
}
|
||||
for flight_type, names in legacy_names.items():
|
||||
loadout_names[flight_type].extend(names)
|
||||
# A SEAD escort typically does not need a different loadout than a regular
|
||||
# SEAD flight, so fall back to SEAD if needed.
|
||||
loadout_names[FlightType.SEAD_ESCORT].extend(loadout_names[FlightType.SEAD])
|
||||
# Sweep and escort can fall back to TARCAP.
|
||||
loadout_names[FlightType.ESCORT].extend(loadout_names[FlightType.TARCAP])
|
||||
loadout_names[FlightType.SWEEP].extend(loadout_names[FlightType.TARCAP])
|
||||
# Intercept can fall back to BARCAP.
|
||||
loadout_names[FlightType.INTERCEPTION].extend(loadout_names[FlightType.BARCAP])
|
||||
# OCA/Aircraft falls back to BAI, which falls back to CAS.
|
||||
loadout_names[FlightType.BAI].extend(loadout_names[FlightType.CAS])
|
||||
loadout_names[FlightType.OCA_AIRCRAFT].extend(loadout_names[FlightType.BAI])
|
||||
# DEAD also falls back to BAI.
|
||||
loadout_names[FlightType.DEAD].extend(loadout_names[FlightType.BAI])
|
||||
# OCA/Runway falls back to Strike
|
||||
loadout_names[FlightType.OCA_RUNWAY].extend(loadout_names[FlightType.STRIKE])
|
||||
yield from loadout_names[flight.flight_type]
|
||||
|
||||
@classmethod
|
||||
def default_for(cls, flight: Flight) -> Loadout:
|
||||
# Iterate through each possible payload type for a given aircraft.
|
||||
# Some aircraft have custom loadouts that in aren't the standard set.
|
||||
for name in cls.default_loadout_names_for(flight):
|
||||
# This operation is cached, but must be called before load_by_name will
|
||||
# work.
|
||||
flight.unit_type.dcs_unit_type.load_payloads()
|
||||
payload = flight.unit_type.dcs_unit_type.loadout_by_name(name)
|
||||
if payload is not None:
|
||||
return Loadout(
|
||||
name,
|
||||
{i: Weapon.with_clsid(d["clsid"]) for i, d in payload},
|
||||
date=None,
|
||||
)
|
||||
|
||||
# TODO: Try group.load_task_default_loadout(loadout_for_task)
|
||||
return cls.empty_loadout()
|
||||
|
||||
@classmethod
|
||||
def empty_loadout(cls) -> Loadout:
|
||||
return Loadout("Empty", {}, date=None)
|
||||
@@ -6,12 +6,13 @@ from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from game.ato import Flight, FlightType
|
||||
from game.ato.packagewaypoints import PackageWaypoints
|
||||
from game.db import Database
|
||||
from game.utils import Speed
|
||||
from gen.flights.flightplan import FormationFlightPlan
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from .flight import Flight
|
||||
from .flightplan import FormationFlightPlan
|
||||
from .flighttype import FlightType
|
||||
from .packagewaypoints import PackageWaypoints
|
||||
from .traveltime import TotEstimator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import MissionTarget
|
||||
|
||||
115
game/ato/traveltime.py
Normal file
115
game/ato/traveltime.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from game.utils import (
|
||||
Distance,
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL,
|
||||
Speed,
|
||||
mach,
|
||||
meters,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .flight import Flight
|
||||
from .package import Package
|
||||
|
||||
|
||||
class GroundSpeed:
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
|
||||
# TODO: Expose both a cruise speed and target speed.
|
||||
# The cruise speed can be used for ascent, hold, join, and RTB to save
|
||||
# on fuel, but mission speed will be fast enough to keep the flight
|
||||
# safer.
|
||||
|
||||
# DCS's max speed is in kph at 0 MSL.
|
||||
max_speed = flight.unit_type.max_speed
|
||||
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
||||
# Aircraft is supersonic. Limit to mach 0.85 to conserve fuel and
|
||||
# account for heavily loaded jets.
|
||||
return mach(0.85, altitude)
|
||||
|
||||
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
||||
# 80% of its maximum, and that it can maintain the same mach at altitude
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
cruise_mach = max_speed.mach() * 0.85
|
||||
return mach(cruise_mach, altitude)
|
||||
|
||||
|
||||
class TravelTime:
|
||||
@staticmethod
|
||||
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
|
||||
error_factor = 1.05
|
||||
distance = meters(a.distance_to_point(b))
|
||||
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
|
||||
|
||||
|
||||
# TODO: Most if not all of this should move into FlightPlan.
|
||||
class TotEstimator:
|
||||
def __init__(self, package: Package) -> None:
|
||||
self.package = package
|
||||
|
||||
@staticmethod
|
||||
def mission_start_time(flight: Flight) -> timedelta:
|
||||
startup_time = flight.flight_plan.startup_time()
|
||||
if startup_time is None:
|
||||
# Could not determine takeoff time, probably due to a custom flight
|
||||
# plan. Start immediately.
|
||||
return timedelta()
|
||||
return startup_time
|
||||
|
||||
def earliest_tot(self) -> timedelta:
|
||||
if not self.package.flights:
|
||||
return timedelta(0)
|
||||
|
||||
earliest_tot = max(
|
||||
(self.earliest_tot_for_flight(f) for f in self.package.flights)
|
||||
)
|
||||
|
||||
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
|
||||
# and they're not interesting from a mission planning perspective so we
|
||||
# don't want them in the UI.
|
||||
#
|
||||
# Round up so we don't get negative start times.
|
||||
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
|
||||
|
||||
@staticmethod
|
||||
def earliest_tot_for_flight(flight: Flight) -> timedelta:
|
||||
"""Estimate fastest time from mission start to the target position.
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
flight: The flight to get the earliest TOT time for.
|
||||
|
||||
Returns:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
"""
|
||||
# Clear the TOT, calculate the startup time. Negating the result gives
|
||||
# the earliest possible start time.
|
||||
orig_tot = flight.package.time_over_target
|
||||
try:
|
||||
flight.package.time_over_target = timedelta()
|
||||
time = flight.flight_plan.startup_time()
|
||||
finally:
|
||||
flight.package.time_over_target = orig_tot
|
||||
|
||||
if time is None:
|
||||
logging.warning(f"Cannot estimate TOT for {flight}")
|
||||
# Return 0 so this flight's travel time does not affect the rest
|
||||
# of the package.
|
||||
return timedelta()
|
||||
return -time
|
||||
620
game/ato/waypointbuilder.py
Normal file
620
game/ato/waypointbuilder.py
Normal file
@@ -0,0 +1,620 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
TheaterGroundObject,
|
||||
TheaterUnit,
|
||||
)
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from .flightwaypoint import AltitudeReference, FlightWaypoint
|
||||
from .flightwaypointtype import FlightWaypointType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
from game.transfers import MultiGroupTransport
|
||||
from game.theater.theatergroup import TheaterGroup
|
||||
from .flight import Flight
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StrikeTarget:
|
||||
name: str
|
||||
target: Union[TheaterGroundObject, TheaterGroup, TheaterUnit, MultiGroupTransport]
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
flight: Flight,
|
||||
coalition: Coalition,
|
||||
targets: Optional[List[StrikeTarget]] = None,
|
||||
) -> None:
|
||||
self.flight = flight
|
||||
self.doctrine = coalition.doctrine
|
||||
self.threat_zones = coalition.opponent.threat_zone
|
||||
self.navmesh = coalition.nav_mesh
|
||||
self.targets = targets
|
||||
self._bullseye = coalition.bullseye
|
||||
|
||||
@property
|
||||
def is_helo(self) -> bool:
|
||||
return self.flight.unit_type.dcs_unit_type.helicopter
|
||||
|
||||
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
|
||||
"""Create takeoff waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Note that the takeoff waypoint will automatically be created by pydcs
|
||||
when we create the group, but creating our own before generation makes
|
||||
the planning code simpler.
|
||||
|
||||
Args:
|
||||
departure: Departure airfield or carrier.
|
||||
"""
|
||||
position = departure.position
|
||||
if isinstance(departure, OffMapSpawn):
|
||||
return FlightWaypoint(
|
||||
"NAV",
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||
description="Enter theater",
|
||||
pretty_name="Enter theater",
|
||||
)
|
||||
|
||||
return FlightWaypoint(
|
||||
"TAKEOFF",
|
||||
FlightWaypointType.TAKEOFF,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(0),
|
||||
alt_type="RADIO",
|
||||
description="Takeoff",
|
||||
pretty_name="Takeoff",
|
||||
)
|
||||
|
||||
def land(self, arrival: ControlPoint) -> FlightWaypoint:
|
||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
position = arrival.position
|
||||
if isinstance(arrival, OffMapSpawn):
|
||||
return FlightWaypoint(
|
||||
"NAV",
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||
description="Exit theater",
|
||||
pretty_name="Exit theater",
|
||||
)
|
||||
|
||||
return FlightWaypoint(
|
||||
"LANDING",
|
||||
FlightWaypointType.LANDING_POINT,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(0),
|
||||
alt_type="RADIO",
|
||||
description="Land",
|
||||
pretty_name="Land",
|
||||
control_point=arrival,
|
||||
)
|
||||
|
||||
def divert(self, divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
|
||||
"""Create divert waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
divert: Divert airfield or carrier.
|
||||
"""
|
||||
if divert is None:
|
||||
return None
|
||||
|
||||
position = divert.position
|
||||
altitude_type: AltitudeReference
|
||||
if isinstance(divert, OffMapSpawn):
|
||||
if self.is_helo:
|
||||
altitude = meters(500)
|
||||
else:
|
||||
altitude = self.doctrine.rendezvous_altitude
|
||||
altitude_type = "BARO"
|
||||
else:
|
||||
altitude = meters(0)
|
||||
altitude_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"DIVERT",
|
||||
FlightWaypointType.DIVERT,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude,
|
||||
alt_type=altitude_type,
|
||||
description="Divert",
|
||||
pretty_name="Divert",
|
||||
only_for_player=True,
|
||||
control_point=divert,
|
||||
)
|
||||
|
||||
def bullseye(self) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
"BULLSEYE",
|
||||
FlightWaypointType.BULLSEYE,
|
||||
self._bullseye.position.x,
|
||||
self._bullseye.position.y,
|
||||
meters(0),
|
||||
description="Bullseye",
|
||||
pretty_name="Bullseye",
|
||||
only_for_player=True,
|
||||
)
|
||||
|
||||
def hold(self, position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"HOLD",
|
||||
FlightWaypointType.LOITER,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||
alt_type,
|
||||
description="Wait until push time",
|
||||
pretty_name="Hold",
|
||||
)
|
||||
|
||||
def join(self, position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"JOIN",
|
||||
FlightWaypointType.JOIN,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description="Rendezvous with package",
|
||||
pretty_name="Join",
|
||||
)
|
||||
|
||||
def refuel(self, position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"REFUEL",
|
||||
FlightWaypointType.REFUEL,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description="Refuel from tanker",
|
||||
pretty_name="Refuel",
|
||||
)
|
||||
|
||||
def split(self, position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"SPLIT",
|
||||
FlightWaypointType.SPLIT,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description="Depart from package",
|
||||
pretty_name="Split",
|
||||
)
|
||||
|
||||
def ingress(
|
||||
self,
|
||||
ingress_type: FlightWaypointType,
|
||||
position: Point,
|
||||
objective: MissionTarget,
|
||||
) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"INGRESS",
|
||||
ingress_type,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description=f"INGRESS on {objective.name}",
|
||||
pretty_name=f"INGRESS on {objective.name}",
|
||||
targets=objective.strike_targets,
|
||||
)
|
||||
|
||||
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"EGRESS",
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description=f"EGRESS from {target.name}",
|
||||
pretty_name=f"EGRESS from {target.name}",
|
||||
)
|
||||
|
||||
def bai_group(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"ATTACK {target.name}")
|
||||
|
||||
def dead_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"STRIKE {target.name}")
|
||||
|
||||
def sead_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"STRIKE {target.name}")
|
||||
|
||||
def strike_point(self, target: StrikeTarget) -> FlightWaypoint:
|
||||
return self._target_point(target, f"STRIKE {target.name}")
|
||||
|
||||
@staticmethod
|
||||
def _target_point(target: StrikeTarget, description: str) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
target.name,
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
target.target.position.x,
|
||||
target.target.position.y,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=description,
|
||||
pretty_name=description,
|
||||
# The target waypoints are only for the player's benefit. AI tasks for
|
||||
# the target are set on the ingress point so that they begin their attack
|
||||
# *before* reaching the target.
|
||||
only_for_player=True,
|
||||
)
|
||||
|
||||
def strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"STRIKE {target.name}", target)
|
||||
|
||||
def sead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"SEAD on {target.name}", target, flyover=True)
|
||||
|
||||
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"DEAD on {target.name}", target)
|
||||
|
||||
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
||||
|
||||
@staticmethod
|
||||
def _target_area(
|
||||
name: str, location: MissionTarget, flyover: bool = False
|
||||
) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
name,
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
location.position.x,
|
||||
location.position.y,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=name,
|
||||
pretty_name=name,
|
||||
)
|
||||
|
||||
# Most target waypoints are only for the player's benefit. AI tasks for
|
||||
# the target are set on the ingress point so they begin their attack
|
||||
# *before* reaching the target.
|
||||
#
|
||||
# The exception is for flight plans that require passing over the
|
||||
# target. For example, OCA strikes need to get close enough to detect
|
||||
# the targets in their engagement zone or they will RTB immediately.
|
||||
if flyover:
|
||||
waypoint.flyover = True
|
||||
else:
|
||||
waypoint.only_for_player = True
|
||||
return waypoint
|
||||
|
||||
def cas(self, position: Point) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
"CAS",
|
||||
FlightWaypointType.CAS,
|
||||
position.x,
|
||||
position.y,
|
||||
meters(60) if self.is_helo else meters(1000),
|
||||
"RADIO",
|
||||
description="Provide CAS",
|
||||
pretty_name="CAS",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a racetrack start waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
return FlightWaypoint(
|
||||
"RACETRACK START",
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude,
|
||||
description="Orbit between this point and the next point",
|
||||
pretty_name="Race-track start",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def race_track_end(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a racetrack end waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
return FlightWaypoint(
|
||||
"RACETRACK END",
|
||||
FlightWaypointType.PATROL,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude,
|
||||
description="Orbit between this point and the previous point",
|
||||
pretty_name="Race-track end",
|
||||
)
|
||||
|
||||
def race_track(
|
||||
self, start: Point, end: Point, altitude: Distance
|
||||
) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
start: The beginning racetrack waypoint.
|
||||
end: The ending racetrack waypoint.
|
||||
altitude: The racetrack altitude.
|
||||
"""
|
||||
return (
|
||||
self.race_track_start(start, altitude),
|
||||
self.race_track_end(end, altitude),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates an circular orbit point.
|
||||
|
||||
Args:
|
||||
start: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
|
||||
return FlightWaypoint(
|
||||
"ORBIT",
|
||||
FlightWaypointType.LOITER,
|
||||
start.x,
|
||||
start.y,
|
||||
altitude,
|
||||
description="Anchor and hold at this point",
|
||||
pretty_name="Orbit",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a sweep start waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the sweep in meters.
|
||||
"""
|
||||
return FlightWaypoint(
|
||||
"SWEEP START",
|
||||
FlightWaypointType.INGRESS_SWEEP,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude,
|
||||
description="Proceed to the target and engage enemy aircraft",
|
||||
pretty_name="Sweep start",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def sweep_end(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a sweep end waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the sweep in meters.
|
||||
"""
|
||||
return FlightWaypoint(
|
||||
"SWEEP END",
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude,
|
||||
description="End of sweep",
|
||||
pretty_name="Sweep end",
|
||||
)
|
||||
|
||||
def sweep(
|
||||
self, start: Point, end: Point, altitude: Distance
|
||||
) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
start: The beginning of the sweep.
|
||||
end: The end of the sweep.
|
||||
altitude: The sweep altitude.
|
||||
"""
|
||||
return self.sweep_start(start, altitude), self.sweep_end(end, altitude)
|
||||
|
||||
def escort(
|
||||
self,
|
||||
ingress: Point,
|
||||
target: MissionTarget,
|
||||
) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates the waypoints needed to escort the package.
|
||||
|
||||
Args:
|
||||
ingress: The package ingress point.
|
||||
target: The mission target.
|
||||
"""
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
# This would preferably be no points at all, and instead the Escort task
|
||||
# would begin on the join point and end on the split point, however the
|
||||
# escort task does not appear to work properly (see the longer
|
||||
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
||||
# the escort flights a flight plan including the ingress point and target area.
|
||||
ingress_wp = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
||||
|
||||
return ingress_wp, FlightWaypoint(
|
||||
"TARGET",
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
target.position.x,
|
||||
target.position.y,
|
||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description="Escort the package",
|
||||
pretty_name="Target area",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def pickup(control_point: ControlPoint) -> FlightWaypoint:
|
||||
"""Creates a cargo pickup waypoint.
|
||||
|
||||
Args:
|
||||
control_point: Pick up location.
|
||||
"""
|
||||
return FlightWaypoint(
|
||||
"PICKUP",
|
||||
FlightWaypointType.PICKUP,
|
||||
control_point.position.x,
|
||||
control_point.position.y,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=f"Pick up cargo from {control_point}",
|
||||
pretty_name="Pick up location",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def drop_off(control_point: ControlPoint) -> FlightWaypoint:
|
||||
"""Creates a cargo drop-off waypoint.
|
||||
|
||||
Args:
|
||||
control_point: Drop-off location.
|
||||
"""
|
||||
return FlightWaypoint(
|
||||
"DROP OFF",
|
||||
FlightWaypointType.PICKUP,
|
||||
control_point.position.x,
|
||||
control_point.position.y,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=f"Drop off cargo at {control_point}",
|
||||
pretty_name="Drop off location",
|
||||
control_point=control_point,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def nav(
|
||||
position: Point, altitude: Distance, altitude_is_agl: bool = False
|
||||
) -> FlightWaypoint:
|
||||
"""Creates a navigation point.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the waypoint.
|
||||
altitude_is_agl: True for altitude is AGL. False if altitude is MSL.
|
||||
"""
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if altitude_is_agl:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"NAV",
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude,
|
||||
alt_type,
|
||||
description="NAV",
|
||||
pretty_name="Nav",
|
||||
)
|
||||
|
||||
def nav_path(
|
||||
self, a: Point, b: Point, altitude: Distance, altitude_is_agl: bool = False
|
||||
) -> List[FlightWaypoint]:
|
||||
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
|
||||
return [self.nav(self.perturb(p), altitude, altitude_is_agl) for p in path]
|
||||
|
||||
def clean_nav_points(self, points: Iterable[Point]) -> Iterator[Point]:
|
||||
# Examine a sliding window of three waypoints. `current` is the waypoint
|
||||
# being checked for prunability. `previous` is the last emitted waypoint
|
||||
# before `current`. `nxt` is the waypoint after `current`.
|
||||
previous: Optional[Point] = None
|
||||
current: Optional[Point] = None
|
||||
for nxt in points:
|
||||
if current is None:
|
||||
current = nxt
|
||||
continue
|
||||
if previous is None:
|
||||
previous = current
|
||||
current = nxt
|
||||
continue
|
||||
|
||||
if self.nav_point_prunable(previous, current, nxt):
|
||||
current = nxt
|
||||
continue
|
||||
|
||||
yield current
|
||||
previous = current
|
||||
current = nxt
|
||||
|
||||
def nav_point_prunable(self, previous: Point, current: Point, nxt: Point) -> bool:
|
||||
previous_threatened = self.threat_zones.path_threatened(previous, current)
|
||||
next_threatened = self.threat_zones.path_threatened(current, nxt)
|
||||
pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
|
||||
previous_distance = meters(previous.distance_to_point(current))
|
||||
distance = meters(current.distance_to_point(nxt))
|
||||
distance_without = previous_distance + distance
|
||||
if distance > distance_without:
|
||||
# Don't prune paths to make them longer.
|
||||
return False
|
||||
|
||||
# We could shorten the path by removing the intermediate
|
||||
# waypoint. Do so if the new path isn't higher threat.
|
||||
if not pruned_threatened:
|
||||
# The new path is not threatened, so safe to prune.
|
||||
return True
|
||||
|
||||
# The new path is threatened. Only allow if both paths were
|
||||
# threatened anyway.
|
||||
return previous_threatened and next_threatened
|
||||
|
||||
@staticmethod
|
||||
def perturb(point: Point) -> Point:
|
||||
deviation = nautical_miles(1)
|
||||
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
return Point(point.x + x_adj, point.y + y_adj)
|
||||
35
game/callsigns.py
Normal file
35
game/callsigns.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Support for working with DCS group callsigns."""
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.flyingunit import FlyingUnit
|
||||
|
||||
|
||||
def callsign_for_support_unit(group: FlyingGroup[Any]) -> str:
|
||||
# Either something like Overlord11 for Western AWACS, or else just a number.
|
||||
# Convert to either "Overlord" or "Flight 123".
|
||||
lead = group.units[0]
|
||||
raw_callsign = lead.callsign_as_str()
|
||||
try:
|
||||
return f"Flight {int(raw_callsign)}"
|
||||
except ValueError:
|
||||
return raw_callsign.rstrip("1234567890")
|
||||
|
||||
|
||||
def create_group_callsign_from_unit(lead: FlyingUnit) -> str:
|
||||
raw_callsign = lead.callsign_as_str()
|
||||
if not lead.callsign_is_western:
|
||||
# Callsigns for non-Western countries are just a number per flight,
|
||||
# similar to tail numbers.
|
||||
return f"Flight {raw_callsign}"
|
||||
|
||||
# Callsign from pydcs is in the format `<name><group ID><unit ID>`,
|
||||
# where unit ID is guaranteed to be a single digit but the group ID may
|
||||
# be more.
|
||||
match = re.search(r"^(\D+)(\d+)(\d)$", raw_callsign)
|
||||
if match is None:
|
||||
logging.error(f"Could not parse unit callsign: {raw_callsign}")
|
||||
return f"Flight {raw_callsign}"
|
||||
return f"{match.group(1)} {match.group(2)}"
|
||||
@@ -2,14 +2,14 @@ from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons.operatingbases import OperatingBases
|
||||
from game.squadrons.squadrondef import SquadronDef
|
||||
from game.theater import ControlPoint
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.factions.faction import Faction
|
||||
@@ -56,7 +56,7 @@ class SquadronDefGenerator:
|
||||
|
||||
@staticmethod
|
||||
def _make_random_nickname() -> str:
|
||||
from gen.naming import ANIMALS
|
||||
from game.naming import ANIMALS
|
||||
|
||||
animal = random.choice(ANIMALS)
|
||||
adjective = random.choice(
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Iterator, Dict, TYPE_CHECKING
|
||||
|
||||
from game.theater import MissionTarget
|
||||
from game.ato.flighttype import FlightType
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from game.ato.traveltime import TotEstimator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
@@ -19,7 +19,7 @@ from game.theater.theatergroundobject import (
|
||||
NavalGroundObject,
|
||||
)
|
||||
from game.utils import meters, nautical_miles
|
||||
from gen.flights.closestairfields import ClosestAirfields, ObjectiveDistanceCache
|
||||
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
@@ -12,7 +12,7 @@ from ..db.database import Database
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons.airwing import AirWing
|
||||
from gen.flights.closestairfields import ClosestAirfields
|
||||
from game.ato.closestairfields import ClosestAirfields
|
||||
from .missionproposals import ProposedFlight
|
||||
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ from game.settings import Settings
|
||||
from game.squadrons import AirWing
|
||||
from game.theater import ConflictTheater
|
||||
from game.threatzones import ThreatZones
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.ato.flightplan import FlightPlanBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import Flight
|
||||
|
||||
@@ -6,8 +6,8 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
from game.theater import FrontLine
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
|
||||
|
||||
class AggressiveAttack(FrontLineStanceTask):
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
|
||||
|
||||
class BreakthroughAttack(FrontLineStanceTask):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
|
||||
|
||||
class DefensiveStance(FrontLineStanceTask):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
|
||||
|
||||
class EliminationAttack(FrontLineStanceTask):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
|
||||
|
||||
class RetreatStance(FrontLineStanceTask):
|
||||
|
||||
@@ -5,11 +5,12 @@ import itertools
|
||||
import math
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
from typing import Optional, TYPE_CHECKING, Union
|
||||
|
||||
from game.commander.garrisons import Garrisons
|
||||
from game.commander.objectivefinder import ObjectiveFinder
|
||||
from game.db import GameDb
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
from game.htn import WorldState
|
||||
from game.profiling import MultiEventTracer
|
||||
from game.settings import Settings
|
||||
@@ -22,7 +23,6 @@ from game.theater.theatergroundobject import (
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from game.threatzones import ThreatZones
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
@@ -15,13 +15,12 @@ from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
from faker import Faker
|
||||
|
||||
from game.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from game.models.game_stats import GameStats
|
||||
from game.plugins import LuaPluginManager
|
||||
from game.utils import Distance
|
||||
from gen import naming
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from . import persistency
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from . import naming, persistency
|
||||
from .ato.flighttype import FlightType
|
||||
from .campaignloader import CampaignAirWingConfig
|
||||
from .coalition import Coalition
|
||||
|
||||
0
game/ground_forces/__init__.py
Normal file
0
game/ground_forces/__init__.py
Normal file
162
game/ground_forces/ai_ground_planner.py
Normal file
162
game/ground_forces/ai_ground_planner.py
Normal file
@@ -0,0 +1,162 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from enum import Enum
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
|
||||
from game.data.units import UnitClass
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from .combat_stance import CombatStance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.theater import ControlPoint
|
||||
|
||||
MAX_COMBAT_GROUP_PER_CP = 10
|
||||
|
||||
|
||||
class CombatGroupRole(Enum):
|
||||
TANK = 1
|
||||
APC = 2
|
||||
IFV = 3
|
||||
ARTILLERY = 4
|
||||
SHORAD = 5
|
||||
LOGI = 6
|
||||
INFANTRY = 7
|
||||
ATGM = 8
|
||||
RECON = 9
|
||||
|
||||
|
||||
DISTANCE_FROM_FRONTLINE = {
|
||||
CombatGroupRole.TANK: (2200, 3200),
|
||||
CombatGroupRole.APC: (2700, 3700),
|
||||
CombatGroupRole.IFV: (2700, 3700),
|
||||
CombatGroupRole.ARTILLERY: (16000, 18000),
|
||||
CombatGroupRole.SHORAD: (5000, 8000),
|
||||
CombatGroupRole.LOGI: (18000, 20000),
|
||||
CombatGroupRole.INFANTRY: (2800, 3300),
|
||||
CombatGroupRole.ATGM: (5200, 6200),
|
||||
CombatGroupRole.RECON: (2000, 3000),
|
||||
}
|
||||
|
||||
GROUP_SIZES_BY_COMBAT_STANCE = {
|
||||
CombatStance.DEFENSIVE: [2, 4, 6],
|
||||
CombatStance.AGGRESSIVE: [2, 4, 6],
|
||||
CombatStance.RETREAT: [2, 4, 6, 8],
|
||||
CombatStance.BREAKTHROUGH: [4, 6, 6, 8],
|
||||
CombatStance.ELIMINATION: [2, 4, 4, 4, 6],
|
||||
CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4],
|
||||
}
|
||||
|
||||
|
||||
class CombatGroup:
|
||||
def __init__(
|
||||
self, role: CombatGroupRole, unit_type: GroundUnitType, size: int
|
||||
) -> None:
|
||||
self.unit_type = unit_type
|
||||
self.size = size
|
||||
self.role = role
|
||||
self.start_position = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
s = f"ROLE : {self.role}\n"
|
||||
if self.size:
|
||||
s += f"UNITS {self.unit_type} * {self.size}"
|
||||
return s
|
||||
|
||||
|
||||
class GroundPlanner:
|
||||
def __init__(self, cp: ControlPoint, game: Game) -> None:
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.connected_enemy_cp = [
|
||||
cp for cp in self.cp.connected_points if cp.captured != self.cp.captured
|
||||
]
|
||||
self.tank_groups: List[CombatGroup] = []
|
||||
self.apc_group: List[CombatGroup] = []
|
||||
self.ifv_group: List[CombatGroup] = []
|
||||
self.art_group: List[CombatGroup] = []
|
||||
self.atgm_group: List[CombatGroup] = []
|
||||
self.logi_groups: List[CombatGroup] = []
|
||||
self.shorad_groups: List[CombatGroup] = []
|
||||
self.recon_groups: List[CombatGroup] = []
|
||||
|
||||
self.units_per_cp: Dict[int, List[CombatGroup]] = {}
|
||||
for cp in self.connected_enemy_cp:
|
||||
self.units_per_cp[cp.id] = []
|
||||
self.reserve: List[CombatGroup] = []
|
||||
|
||||
def plan_groundwar(self) -> None:
|
||||
|
||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||
|
||||
remaining_available_frontline_units = ground_unit_limit
|
||||
|
||||
# TODO: Fix to handle the per-front stances.
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/1417
|
||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
|
||||
|
||||
# Create combat groups and assign them randomly to each enemy CP
|
||||
for unit_type in self.cp.base.armor:
|
||||
unit_class = unit_type.unit_class
|
||||
if unit_class is UnitClass.TANK:
|
||||
collection = self.tank_groups
|
||||
role = CombatGroupRole.TANK
|
||||
elif unit_class is UnitClass.APC:
|
||||
collection = self.apc_group
|
||||
role = CombatGroupRole.APC
|
||||
elif unit_class is UnitClass.ARTILLERY:
|
||||
collection = self.art_group
|
||||
role = CombatGroupRole.ARTILLERY
|
||||
elif unit_class is UnitClass.IFV:
|
||||
collection = self.ifv_group
|
||||
role = CombatGroupRole.IFV
|
||||
elif unit_class is UnitClass.LOGISTICS:
|
||||
collection = self.logi_groups
|
||||
role = CombatGroupRole.LOGI
|
||||
elif unit_class is UnitClass.ATGM:
|
||||
collection = self.atgm_group
|
||||
role = CombatGroupRole.ATGM
|
||||
elif unit_class is UnitClass.SHORAD:
|
||||
collection = self.shorad_groups
|
||||
role = CombatGroupRole.SHORAD
|
||||
elif unit_class is UnitClass.RECON:
|
||||
collection = self.recon_groups
|
||||
role = CombatGroupRole.RECON
|
||||
else:
|
||||
logging.warning(
|
||||
f"Unused front line vehicle at base {unit_type}: unknown unit class"
|
||||
)
|
||||
continue
|
||||
|
||||
available = self.cp.base.armor[unit_type]
|
||||
|
||||
if available > remaining_available_frontline_units:
|
||||
available = remaining_available_frontline_units
|
||||
|
||||
remaining_available_frontline_units -= available
|
||||
|
||||
while available > 0:
|
||||
|
||||
if role == CombatGroupRole.SHORAD:
|
||||
count = 1
|
||||
else:
|
||||
count = random.choice(group_size_choice)
|
||||
if count > available:
|
||||
if available >= 2:
|
||||
count = 2
|
||||
else:
|
||||
count = 1
|
||||
available -= count
|
||||
|
||||
group = CombatGroup(role, unit_type, count)
|
||||
if len(self.connected_enemy_cp) > 0:
|
||||
enemy_cp = random.choice(self.connected_enemy_cp).id
|
||||
self.units_per_cp[enemy_cp].append(group)
|
||||
else:
|
||||
self.reserve.append(group)
|
||||
collection.append(group)
|
||||
|
||||
if remaining_available_frontline_units == 0:
|
||||
break
|
||||
12
game/ground_forces/combat_stance.py
Normal file
12
game/ground_forces/combat_stance.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class CombatStance(Enum):
|
||||
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
|
||||
AGGRESSIVE = (
|
||||
1 # Unit will attempt to make progress with medium sized group of units
|
||||
)
|
||||
RETREAT = 2 # Unit will retreat
|
||||
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively
|
||||
ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force
|
||||
AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller)
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from dcs.task import (
|
||||
@@ -24,7 +23,7 @@ from dcs.task import (
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
from game.ato import Flight, FlightType
|
||||
from gen.flights.flightplan import AwacsFlightPlan, RefuelingFlightPlan
|
||||
from game.ato.flightplan import AwacsFlightPlan, RefuelingFlightPlan
|
||||
|
||||
|
||||
class AircraftBehavior:
|
||||
|
||||
@@ -21,13 +21,13 @@ from game.missiongenerator.airsupport import AirSupport
|
||||
from game.missiongenerator.lasercoderegistry import LaserCodeRegistry
|
||||
from game.radio.radios import RadioRegistry
|
||||
from game.radio.tacan import TacanRegistry
|
||||
from game.runways import RunwayData
|
||||
from game.settings import Settings
|
||||
from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
)
|
||||
from game.unitmap import UnitMap
|
||||
from gen.runways import RunwayData
|
||||
from .aircraftpainter import AircraftPainter
|
||||
from .flightdata import FlightData
|
||||
from .flightgroupconfigurator import FlightGroupConfigurator
|
||||
|
||||
@@ -6,13 +6,13 @@ from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.flyingunit import FlyingUnit
|
||||
|
||||
from gen.callsigns import create_group_callsign_from_unit
|
||||
from game.callsigns import create_group_callsign_from_unit
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import FlightType, FlightWaypoint, Package
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.radio.radios import RadioFrequency
|
||||
from gen.runways import RunwayData
|
||||
from game.runways import RunwayData
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -11,15 +11,15 @@ from dcs.unit import Skill
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
from game.ato import Flight, FlightType
|
||||
from game.callsigns import callsign_for_support_unit
|
||||
from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum
|
||||
from game.missiongenerator.airsupport import AirSupport, AwacsInfo, TankerInfo
|
||||
from game.missiongenerator.lasercoderegistry import LaserCodeRegistry
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
|
||||
from game.runways import RunwayData
|
||||
from game.squadrons import Pilot
|
||||
from gen.callsigns import callsign_for_support_unit
|
||||
from gen.flights.flightplan import AwacsFlightPlan, RefuelingFlightPlan
|
||||
from gen.runways import RunwayData
|
||||
from game.ato.flightplan import AwacsFlightPlan, RefuelingFlightPlan
|
||||
from .aircraftbehavior import AircraftBehavior
|
||||
from .aircraftpainter import AircraftPainter
|
||||
from .flightdata import FlightData
|
||||
|
||||
@@ -15,10 +15,10 @@ from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||
from game.ato import Flight
|
||||
from game.ato.flightstate import InFlight
|
||||
from game.ato.starttype import StartType
|
||||
from game.naming import namegen
|
||||
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, OffMapSpawn
|
||||
from game.utils import feet, meters
|
||||
from gen.flights.traveltime import GroundSpeed
|
||||
from gen.naming import namegen
|
||||
from game.ato.traveltime import GroundSpeed
|
||||
|
||||
WARM_START_HELI_ALT = meters(500)
|
||||
WARM_START_ALTITUDE = meters(3000)
|
||||
|
||||
@@ -4,7 +4,7 @@ from dcs.point import MovingPoint
|
||||
from dcs.task import EngageTargets, EngageTargetsInZone, Targets
|
||||
|
||||
from game.utils import nautical_miles
|
||||
from gen.flights.flightplan import CasFlightPlan
|
||||
from game.ato.flightplan import CasFlightPlan
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import ControlledTask, OptFormation, OrbitAction
|
||||
|
||||
from gen.flights.flightplan import LoiterFlightPlan
|
||||
from game.ato.flightplan import LoiterFlightPlan
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from dcs.task import (
|
||||
)
|
||||
|
||||
from game.ato import FlightType
|
||||
from gen.flights.flightplan import PatrollingFlightPlan
|
||||
from game.ato.flightplan import PatrollingFlightPlan
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from dcs.point import MovingPoint
|
||||
|
||||
from gen.flights.flightplan import PatrollingFlightPlan
|
||||
from game.ato.flightplan import PatrollingFlightPlan
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from dcs.point import MovingPoint
|
||||
from dcs.task import EngageTargets, OptFormation, Targets
|
||||
|
||||
from game.utils import nautical_miles
|
||||
from gen.flights.flightplan import SweepFlightPlan
|
||||
from game.ato.flightplan import SweepFlightPlan
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import List, Type, Tuple, TYPE_CHECKING
|
||||
from typing import List, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135, PlaneType
|
||||
@@ -15,14 +15,13 @@ from dcs.task import (
|
||||
)
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game.utils import Heading
|
||||
from gen.callsigns import callsign_for_support_unit
|
||||
from gen.flights.ai_flight_planner_db import AEWC_CAPABLE
|
||||
from gen.naming import namegen
|
||||
from game.callsigns import callsign_for_support_unit
|
||||
from game.naming import namegen
|
||||
from game.radio.radios import RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
|
||||
|
||||
from .airsupport import AirSupport, TankerInfo, AwacsInfo
|
||||
from game.utils import Heading
|
||||
from game.ato.ai_flight_planner_db import AEWC_CAPABLE
|
||||
from .airsupport import AirSupport, AwacsInfo, TankerInfo
|
||||
from .frontlineconflictdescription import FrontLineConflictDescription
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -11,17 +11,15 @@ from typing import Dict, List, TYPE_CHECKING
|
||||
from dcs.mission import Mission
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
from game.theater import ControlPoint, FrontLine
|
||||
from game.ato.flightwaypoint import FlightWaypoint
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
from game.radio.radios import RadioFrequency
|
||||
from gen.runways import RunwayData
|
||||
|
||||
from game.runways import RunwayData
|
||||
from game.theater import ControlPoint, FrontLine
|
||||
from .aircraft.flightdata import FlightData
|
||||
from .airsupportgenerator import AwacsInfo, TankerInfo
|
||||
from .flotgenerator import JtacInfo
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
from typing import List, Optional, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.action import AITaskPush
|
||||
@@ -13,9 +13,9 @@ from dcs.mapping import Point
|
||||
from dcs.point import PointAction
|
||||
from dcs.task import (
|
||||
AFAC,
|
||||
EPLRS,
|
||||
AttackGroup,
|
||||
ControlledTask,
|
||||
EPLRS,
|
||||
FAC,
|
||||
FireAtPoint,
|
||||
GoToWaypoint,
|
||||
@@ -25,25 +25,24 @@ from dcs.task import (
|
||||
SetInvisibleCommand,
|
||||
)
|
||||
from dcs.triggers import Event, TriggerOnce
|
||||
from dcs.unit import Vehicle, Skill
|
||||
from dcs.unit import Skill, Vehicle
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game.callsigns import callsign_for_support_unit
|
||||
from game.data.units import UnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.ground_forces.ai_ground_planner import (
|
||||
CombatGroup,
|
||||
CombatGroupRole,
|
||||
DISTANCE_FROM_FRONTLINE,
|
||||
)
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
from game.naming import namegen
|
||||
from game.radio.radios import RadioRegistry
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import Heading
|
||||
from gen.ground_forces.ai_ground_planner import (
|
||||
DISTANCE_FROM_FRONTLINE,
|
||||
CombatGroup,
|
||||
CombatGroupRole,
|
||||
)
|
||||
from gen.callsigns import callsign_for_support_unit
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from gen.naming import namegen
|
||||
from game.radio.radios import RadioRegistry
|
||||
|
||||
from .airsupport import AirSupport, JtacInfo
|
||||
from .frontlineconflictdescription import FrontLineConflictDescription
|
||||
from .lasercoderegistry import LaserCodeRegistry
|
||||
|
||||
@@ -40,15 +40,14 @@ from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.data.alic import AlicCodes
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.radio.radios import RadioFrequency
|
||||
from game.runways import RunwayData
|
||||
from game.theater import ConflictTheater, LatLon, TheaterGroundObject, TheaterUnit
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.utils import Distance, UnitSystem, meters, mps, pounds
|
||||
from game.weather import Weather
|
||||
from gen.runways import RunwayData
|
||||
from .aircraft.flightdata import FlightData
|
||||
from .airsupportgenerator import AwacsInfo, TankerInfo
|
||||
from .briefinggenerator import CommInfo, JtacInfo, MissionInfoGenerator
|
||||
from ..dcs.helpers import unit_type_from_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
@@ -10,17 +10,17 @@ from dcs import Mission, Point
|
||||
from dcs.coalition import Coalition
|
||||
from dcs.countries import country_dict
|
||||
|
||||
from game.airfields import AirfieldData
|
||||
from game.dcs.helpers import unit_type_from_name
|
||||
from game.missiongenerator.aircraft.aircraftgenerator import (
|
||||
AircraftGenerator,
|
||||
)
|
||||
from game.naming import namegen
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.tacan import TacanRegistry
|
||||
from game.theater import Airfield, FrontLine
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.unitmap import UnitMap
|
||||
from gen.airfields import AirfieldData
|
||||
from gen.naming import namegen
|
||||
from .aircraft.flightdata import FlightData
|
||||
from .airsupport import AirSupport
|
||||
from .airsupportgenerator import AirSupportGenerator
|
||||
|
||||
@@ -43,6 +43,7 @@ from dcs.vehicles import vehicle_map
|
||||
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
|
||||
from game.runways import RunwayData
|
||||
from game.theater import ControlPoint, TheaterGroundObject, TheaterUnit
|
||||
from game.theater.theatergroundobject import (
|
||||
CarrierGroundObject,
|
||||
@@ -53,7 +54,6 @@ from game.theater.theatergroundobject import (
|
||||
from game.theater.theatergroup import SceneryUnit, TheaterGroup
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import Heading, feet, knots, mps
|
||||
from gen.runways import RunwayData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
567
game/naming.py
Normal file
567
game/naming.py
Normal file
@@ -0,0 +1,567 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import time
|
||||
from typing import List, Any, TYPE_CHECKING
|
||||
|
||||
from dcs.country import Country
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.unittype import UnitType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
|
||||
ALPHA_MILITARY = [
|
||||
"Alpha",
|
||||
"Bravo",
|
||||
"Charlie",
|
||||
"Delta",
|
||||
"Echo",
|
||||
"Foxtrot",
|
||||
"Golf",
|
||||
"Hotel",
|
||||
"India",
|
||||
"Juliet",
|
||||
"Kilo",
|
||||
"Lima",
|
||||
"Mike",
|
||||
"November",
|
||||
"Oscar",
|
||||
"Papa",
|
||||
"Quebec",
|
||||
"Romeo",
|
||||
"Sierra",
|
||||
"Tango",
|
||||
"Uniform",
|
||||
"Victor",
|
||||
"Whisky",
|
||||
"XRay",
|
||||
"Yankee",
|
||||
"Zulu",
|
||||
"Zero",
|
||||
]
|
||||
|
||||
ANIMALS: tuple[str, ...] = (
|
||||
"AARDVARK",
|
||||
"AARDWOLF",
|
||||
"ADDER",
|
||||
"ALBACORE",
|
||||
"ALBATROSS",
|
||||
"ALLIGATOR",
|
||||
"ALPACA",
|
||||
"ANACONDA",
|
||||
"ANOLE",
|
||||
"ANTEATER",
|
||||
"ANTELOPE",
|
||||
"ANTLION",
|
||||
"ARAPAIMA",
|
||||
"ARCHERFISH",
|
||||
"ARGALI",
|
||||
"ARMADILLO",
|
||||
"ASP",
|
||||
"AUROCHS",
|
||||
"AXOLOTL",
|
||||
"BABIRUSA",
|
||||
"BABOON",
|
||||
"BADGER",
|
||||
"BANDICOOT",
|
||||
"BARRACUDA",
|
||||
"BARRAMUNDI",
|
||||
"BASILISK",
|
||||
"BASS",
|
||||
"BAT",
|
||||
"BEAR",
|
||||
"BEAVER",
|
||||
"BEETLE",
|
||||
"BELUGA",
|
||||
"BETTONG",
|
||||
"BINTURONG",
|
||||
"BISON",
|
||||
"BLOODHOUND",
|
||||
"BOA",
|
||||
"BOBCAT",
|
||||
"BONGO",
|
||||
"BONITO",
|
||||
"BUFFALO",
|
||||
"BULLDOG",
|
||||
"BULLFROG",
|
||||
"BULLSHARK",
|
||||
"BUMBLEBEE",
|
||||
"BUNNY",
|
||||
"BUTTERFLY",
|
||||
"CAIMAN",
|
||||
"CAMEL",
|
||||
"CANARY",
|
||||
"CAPYBARA",
|
||||
"CARACAL",
|
||||
"CARP",
|
||||
"CASTOR",
|
||||
"CAT",
|
||||
"CATERPILLAR",
|
||||
"CATFISH",
|
||||
"CENTIPEDE",
|
||||
"CHAMELEON",
|
||||
"CHEETAH",
|
||||
"CHICKEN",
|
||||
"CHIMAERA",
|
||||
"CICADA",
|
||||
"CICHLID",
|
||||
"CIVET",
|
||||
"COBIA",
|
||||
"COBRA",
|
||||
"COCKATOO",
|
||||
"COD",
|
||||
"COELACANTH",
|
||||
"COLT",
|
||||
"CONDOR",
|
||||
"COPPERHEAD",
|
||||
"CORAL",
|
||||
"CORGI",
|
||||
"COTTONMOUTH",
|
||||
"COUGAR",
|
||||
"COW",
|
||||
"COYOTE",
|
||||
"CRAB",
|
||||
"CRANE",
|
||||
"CRICKET",
|
||||
"CROCODILE",
|
||||
"CROW",
|
||||
"CUTTLEFISH",
|
||||
"DACHSHUND",
|
||||
"DEER",
|
||||
"DINGO",
|
||||
"DIREWOLF",
|
||||
"DODO",
|
||||
"DOG",
|
||||
"DOLPHIN",
|
||||
"DONKEY",
|
||||
"DOVE",
|
||||
"DRACO",
|
||||
"DRAGON",
|
||||
"DRAGONFLY",
|
||||
"DUCK",
|
||||
"DUGONG",
|
||||
"EAGLE",
|
||||
"EARWIG",
|
||||
"ECHIDNA",
|
||||
"EEL",
|
||||
"ELEPHANT",
|
||||
"ELK",
|
||||
"EMU",
|
||||
"ERMINE",
|
||||
"FALCON",
|
||||
"FANGTOOTH",
|
||||
"FAWN",
|
||||
"FENNEC",
|
||||
"FERRET",
|
||||
"FINCH",
|
||||
"FIREFLY",
|
||||
"FISH",
|
||||
"FLAMINGO",
|
||||
"FLEA",
|
||||
"FLOUNDER",
|
||||
"FOX",
|
||||
"FRINGEHEAD",
|
||||
"FROG",
|
||||
"FROGMOUTH",
|
||||
"GAR",
|
||||
"GAZELLE",
|
||||
"GECKO",
|
||||
"GENET",
|
||||
"GERBIL",
|
||||
"GHARIAL",
|
||||
"GIBBON",
|
||||
"GIRAFFE",
|
||||
"GOOSE",
|
||||
"GOPHER",
|
||||
"GORILLA",
|
||||
"GOSHAWK",
|
||||
"GRASSHOPPER",
|
||||
"GREYHOUND",
|
||||
"GRIZZLY",
|
||||
"GROUPER",
|
||||
"GROUSE",
|
||||
"GRYPHON",
|
||||
"GUANACO",
|
||||
"GULL",
|
||||
"GUPPY",
|
||||
"HADDOCK",
|
||||
"HAGFISH",
|
||||
"HALIBUT",
|
||||
"HAMSTER",
|
||||
"HARAMBE",
|
||||
"HARE",
|
||||
"HARRIER",
|
||||
"HAWK",
|
||||
"HEDGEHOG",
|
||||
"HERMITCRAB",
|
||||
"HERON",
|
||||
"HERRING",
|
||||
"HIPPO",
|
||||
"HORNBILL",
|
||||
"HORNET",
|
||||
"HORSE",
|
||||
"HUNTSMAN",
|
||||
"HUSKY",
|
||||
"HYENA",
|
||||
"IBEX",
|
||||
"IBIS",
|
||||
"IGUANA",
|
||||
"IMPALA",
|
||||
"INSECT",
|
||||
"IRUKANDJI",
|
||||
"ISOPOD",
|
||||
"JACKAL",
|
||||
"JAGUAR",
|
||||
"JELLYFISH",
|
||||
"JERBOA",
|
||||
"KAKAPO",
|
||||
"KANGAROO",
|
||||
"KATYDID",
|
||||
"KEA",
|
||||
"KINGFISHER",
|
||||
"KITTEN",
|
||||
"KIWI",
|
||||
"KOALA",
|
||||
"KOMODO",
|
||||
"KRAIT",
|
||||
"LADYBUG",
|
||||
"LAMPREY",
|
||||
"LEMUR",
|
||||
"LEOPARD",
|
||||
"LIGHTFOOT",
|
||||
"LION",
|
||||
"LIONFISH",
|
||||
"LIZARD",
|
||||
"LLAMA",
|
||||
"LOACH",
|
||||
"LOBSTER",
|
||||
"LOCUST",
|
||||
"LORIKEET",
|
||||
"LUNGFISH",
|
||||
"LYNX",
|
||||
"MACAW",
|
||||
"MAGPIE",
|
||||
"MALLARD",
|
||||
"MAMBA",
|
||||
"MAMMOTH",
|
||||
"MANATEE",
|
||||
"MANDRILL",
|
||||
"MANTA",
|
||||
"MANTIS",
|
||||
"MARE",
|
||||
"MARLIN",
|
||||
"MARMOT",
|
||||
"MARTEN",
|
||||
"MASTIFF",
|
||||
"MASTODON",
|
||||
"MAVERICK",
|
||||
"MAYFLY",
|
||||
"MEERKAT",
|
||||
"MILLIPEDE",
|
||||
"MINK",
|
||||
"MOA",
|
||||
"MOCKINGBIRD",
|
||||
"MOLE",
|
||||
"MOLERAT",
|
||||
"MOLLY",
|
||||
"MONGOOSE",
|
||||
"MONKEY",
|
||||
"MONKFISH",
|
||||
"MONSTER",
|
||||
"MOOSE",
|
||||
"MORAY",
|
||||
"MOSQUITO",
|
||||
"MOTH",
|
||||
"MOUSE",
|
||||
"MUDSKIPPER",
|
||||
"MULE",
|
||||
"MUSK",
|
||||
"MYNA",
|
||||
"NARWHAL",
|
||||
"NAUTILUS",
|
||||
"NEWT",
|
||||
"NIGHTINGALE",
|
||||
"NUMBAT",
|
||||
"OCELOT",
|
||||
"OCTOPUS",
|
||||
"OKAPI",
|
||||
"OLM",
|
||||
"OPAH",
|
||||
"OPOSSUM",
|
||||
"ORCA",
|
||||
"ORYX",
|
||||
"OSPREY",
|
||||
"OSTRICH",
|
||||
"OTTER",
|
||||
"OWL",
|
||||
"OX",
|
||||
"OYSTER",
|
||||
"PADDLEFISH",
|
||||
"PADEMELON",
|
||||
"PANDA",
|
||||
"PANGOLIN",
|
||||
"PANTHER",
|
||||
"PARAKEET",
|
||||
"PARROT",
|
||||
"PEACOCK",
|
||||
"PELICAN",
|
||||
"PENGUIN",
|
||||
"PERCH",
|
||||
"PEREGRINE",
|
||||
"PETRAL",
|
||||
"PHEASANT",
|
||||
"PIG",
|
||||
"PIGEON",
|
||||
"PIGLET",
|
||||
"PIKE",
|
||||
"PIRANHA",
|
||||
"PLATYPUS",
|
||||
"POODLE",
|
||||
"PORCUPINE",
|
||||
"PORPOISE",
|
||||
"POSSUM",
|
||||
"POTOROO",
|
||||
"PRONGHORN",
|
||||
"PUFFERFISH",
|
||||
"PUFFIN",
|
||||
"PUG",
|
||||
"PUMA",
|
||||
"PYTHON",
|
||||
"QUAGGA",
|
||||
"QUAIL",
|
||||
"QUOKKA",
|
||||
"QUOLL",
|
||||
"RABBIT",
|
||||
"RACOON",
|
||||
"RAGDOLL",
|
||||
"RAT",
|
||||
"RATTLESNAKE",
|
||||
"RAVEN",
|
||||
"REINDEER",
|
||||
"RHINO",
|
||||
"ROACH",
|
||||
"ROBIN",
|
||||
"SABERTOOTH",
|
||||
"SAILFISH",
|
||||
"SALAMANDER",
|
||||
"SALMON",
|
||||
"SANDFLY",
|
||||
"SARDINE",
|
||||
"SAWFISH",
|
||||
"SCARAB",
|
||||
"SCORPION",
|
||||
"SEAHORSE",
|
||||
"SEAL",
|
||||
"SEALION",
|
||||
"SERVAL",
|
||||
"SHARK",
|
||||
"SHEEP",
|
||||
"SHOEBILL",
|
||||
"SHRIKE",
|
||||
"SHRIMP",
|
||||
"SIDEWINDER",
|
||||
"SILKWORM",
|
||||
"SKATE",
|
||||
"SKINK",
|
||||
"SKUNK",
|
||||
"SLOTH",
|
||||
"SLUG",
|
||||
"SNAIL",
|
||||
"SNAKE",
|
||||
"SNAPPER",
|
||||
"SNOOK",
|
||||
"SPARROW",
|
||||
"SPIDER",
|
||||
"SPRINGBOK",
|
||||
"SQUID",
|
||||
"SQUIRREL",
|
||||
"STAGHORN",
|
||||
"STARFISH",
|
||||
"STINGRAY",
|
||||
"STINKBUG",
|
||||
"STOUT",
|
||||
"STURGEON",
|
||||
"SUGARGLIDER",
|
||||
"SUNBEAR",
|
||||
"SWALLOW",
|
||||
"SWAN",
|
||||
"SWIFT",
|
||||
"SWORDFISH",
|
||||
"TAIPAN",
|
||||
"TAKAHE",
|
||||
"TAMARIN",
|
||||
"TANG",
|
||||
"TAPIR",
|
||||
"TARANTULA",
|
||||
"TARPON",
|
||||
"TARSIER",
|
||||
"TAURUS",
|
||||
"TERMITE",
|
||||
"TERRIER",
|
||||
"TETRA",
|
||||
"THRUSH",
|
||||
"THYLACINE",
|
||||
"TIGER",
|
||||
"TOAD",
|
||||
"TORTOISE",
|
||||
"TOUCAN",
|
||||
"TREADFIN",
|
||||
"TREVALLY",
|
||||
"TRIGGERFISH",
|
||||
"TROUT",
|
||||
"TUATARA",
|
||||
"TUNA",
|
||||
"TURKEY",
|
||||
"TURTLE",
|
||||
"URCHIN",
|
||||
"VIPER",
|
||||
"VULTURE",
|
||||
"WALLABY",
|
||||
"WALLAROO",
|
||||
"WALLEYE",
|
||||
"WALRUS",
|
||||
"WARTHOG",
|
||||
"WASP",
|
||||
"WATERBUCK",
|
||||
"WEASEL",
|
||||
"WEEVIL",
|
||||
"WEKA",
|
||||
"WHALE",
|
||||
"WILDCAT",
|
||||
"WILDEBEEST",
|
||||
"WOLF",
|
||||
"WOLFHOUND",
|
||||
"WOLVERINE",
|
||||
"WOMBAT",
|
||||
"WOODCHUCK",
|
||||
"WOODPECKER",
|
||||
"WORM",
|
||||
"WRASSE",
|
||||
"WYVERN",
|
||||
"YAK",
|
||||
"ZEBRA",
|
||||
"ZEBU",
|
||||
)
|
||||
|
||||
|
||||
class NameGenerator:
|
||||
number = 0
|
||||
infantry_number = 0
|
||||
aircraft_number = 0
|
||||
convoy_number = 0
|
||||
cargo_ship_number = 0
|
||||
|
||||
animals: list[str] = list(ANIMALS)
|
||||
existing_alphas: List[str] = []
|
||||
|
||||
@classmethod
|
||||
def reset(cls) -> None:
|
||||
cls.number = 0
|
||||
cls.infantry_number = 0
|
||||
cls.convoy_number = 0
|
||||
cls.cargo_ship_number = 0
|
||||
cls.animals = list(ANIMALS)
|
||||
cls.existing_alphas = []
|
||||
|
||||
@classmethod
|
||||
def reset_numbers(cls) -> None:
|
||||
cls.number = 0
|
||||
cls.infantry_number = 0
|
||||
cls.aircraft_number = 0
|
||||
cls.convoy_number = 0
|
||||
cls.cargo_ship_number = 0
|
||||
|
||||
@classmethod
|
||||
def next_aircraft_name(
|
||||
cls, country: Country, parent_base_id: int, flight: Flight
|
||||
) -> str:
|
||||
cls.aircraft_number += 1
|
||||
try:
|
||||
if flight.custom_name:
|
||||
name_str = flight.custom_name
|
||||
else:
|
||||
name_str = "{} {}".format(
|
||||
flight.package.target.name, flight.flight_type
|
||||
)
|
||||
except AttributeError: # Here to maintain save compatibility with 2.3
|
||||
name_str = "{} {}".format(flight.package.target.name, flight.flight_type)
|
||||
return "{}|{}|{}|{}|{}|".format(
|
||||
name_str,
|
||||
country.id,
|
||||
cls.aircraft_number,
|
||||
parent_base_id,
|
||||
flight.unit_type.name,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def next_unit_name(
|
||||
cls, country: Country, parent_base_id: int, unit_type: UnitType[Any]
|
||||
) -> str:
|
||||
cls.number += 1
|
||||
return "unit|{}|{}|{}|{}|".format(
|
||||
country.id, cls.number, parent_base_id, unit_type.name
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def next_infantry_name(
|
||||
cls, country: Country, parent_base_id: int, unit_type: UnitType[Any]
|
||||
) -> str:
|
||||
cls.infantry_number += 1
|
||||
return "infantry|{}|{}|{}|{}|".format(
|
||||
country.id,
|
||||
cls.infantry_number,
|
||||
parent_base_id,
|
||||
unit_type.name,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def next_awacs_name(cls, country: Country) -> str:
|
||||
cls.number += 1
|
||||
return "awacs|{}|{}|0|".format(country.id, cls.number)
|
||||
|
||||
@classmethod
|
||||
def next_tanker_name(cls, country: Country, unit_type: AircraftType) -> str:
|
||||
cls.number += 1
|
||||
return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.name)
|
||||
|
||||
@classmethod
|
||||
def next_carrier_name(cls, country: Country) -> str:
|
||||
cls.number += 1
|
||||
return "carrier|{}|{}|0|".format(country.id, cls.number)
|
||||
|
||||
@classmethod
|
||||
def next_convoy_name(cls) -> str:
|
||||
cls.convoy_number += 1
|
||||
return f"Convoy {cls.convoy_number:03}"
|
||||
|
||||
@classmethod
|
||||
def next_cargo_ship_name(cls) -> str:
|
||||
cls.cargo_ship_number += 1
|
||||
return f"Cargo Ship {cls.cargo_ship_number:03}"
|
||||
|
||||
@classmethod
|
||||
def random_objective_name(cls) -> str:
|
||||
if cls.animals:
|
||||
animal = random.choice(cls.animals)
|
||||
cls.animals.remove(animal)
|
||||
return animal
|
||||
|
||||
for _ in range(10):
|
||||
alpha = random.choice(ALPHA_MILITARY).upper()
|
||||
number = random.randint(0, 100)
|
||||
alpha_mil_name = f"{alpha} #{number:02}"
|
||||
if alpha_mil_name not in cls.existing_alphas:
|
||||
cls.existing_alphas.append(alpha_mil_name)
|
||||
return alpha_mil_name
|
||||
|
||||
# At this point, give up trying - something has gone wrong and we haven't been
|
||||
# able to make a new name in 10 tries. We'll just make a longer name using the
|
||||
# current unix epoch in nanoseconds. That should be unique... right?
|
||||
last_chance_name = alpha_mil_name + str(time.time_ns())
|
||||
cls.existing_alphas.append(last_chance_name)
|
||||
return last_chance_name
|
||||
|
||||
|
||||
namegen = NameGenerator
|
||||
134
game/runways.py
Normal file
134
game/runways.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Runway information and selection."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.terrain.terrain import Airport
|
||||
|
||||
from game.airfields import AirfieldData
|
||||
from game.radio.radios import RadioFrequency
|
||||
from game.radio.tacan import TacanChannel
|
||||
from game.utils import Heading
|
||||
from game.weather import Conditions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RunwayData:
|
||||
airfield_name: str
|
||||
runway_heading: Heading
|
||||
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,
|
||||
theater: ConflictTheater,
|
||||
airport: Airport,
|
||||
runway_heading: Heading,
|
||||
runway_name: str,
|
||||
) -> RunwayData:
|
||||
"""Creates RunwayData for the given runway of an airfield.
|
||||
|
||||
Args:
|
||||
theater: The theater the airport is in.
|
||||
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 = AirfieldData.for_airport(theater, airport)
|
||||
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} ({airport.id}")
|
||||
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, theater: ConflictTheater, 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(
|
||||
theater, airport, Heading.from_degrees(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 = Heading.from_degrees(runway.heading).opposite
|
||||
runway_number = heading.degrees // 10
|
||||
runway_side = ["", "R", "L"][runway.leftright]
|
||||
runway_name = f"{runway_number:02}{runway_side}"
|
||||
yield cls.for_airfield(theater, airport, heading, runway_name)
|
||||
|
||||
|
||||
class RunwayAssigner:
|
||||
def __init__(self, conditions: Conditions):
|
||||
self.conditions = conditions
|
||||
|
||||
def angle_off_headwind(self, runway: RunwayData) -> Heading:
|
||||
wind = Heading.from_degrees(self.conditions.weather.wind.at_0m.direction)
|
||||
ideal_heading = wind.opposite
|
||||
return runway.runway_heading.angle_between(ideal_heading)
|
||||
|
||||
def get_preferred_runway(
|
||||
self, theater: ConflictTheater, 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(theater, 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]
|
||||
@@ -6,7 +6,7 @@ from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
from game import Game
|
||||
from game.server import GameContext
|
||||
from game.server.leaflet import LeafletPoly, ShapelyUtil
|
||||
from gen.flights.flightplan import CasFlightPlan, PatrollingFlightPlan
|
||||
from game.ato.flightplan import CasFlightPlan, PatrollingFlightPlan
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/flights")
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from game.ato.flightstate import (
|
||||
WaitingForStart,
|
||||
)
|
||||
from game.ato.starttype import StartType
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from game.ato.traveltime import TotEstimator
|
||||
from .combat import CombatInitiator, FrozenCombat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -4,8 +4,8 @@ import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.debriefing import Debriefing
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
from game.theater import ControlPoint
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from ..ato.airtaaskingorder import AirTaskingOrder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -5,9 +5,8 @@ from collections import defaultdict
|
||||
from typing import Sequence, Iterator, TYPE_CHECKING, Optional
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from .squadrondef import SquadronDef
|
||||
from game.ato.ai_flight_planner_db import aircraft_for_task
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from .squadrondefloader import SquadronDefLoader
|
||||
from ..campaignloader.squadrondefgenerator import SquadronDefGenerator
|
||||
from ..factions.faction import Faction
|
||||
|
||||
@@ -10,7 +10,7 @@ from faker import Faker
|
||||
|
||||
from game.ato import Flight, FlightType, Package
|
||||
from game.settings import AutoAtoBehavior, Settings
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from game.ato.flightplan import FlightPlanBuilder
|
||||
from .pilot import Pilot, PilotStatus
|
||||
from ..db.database import Database
|
||||
from ..utils import meters
|
||||
|
||||
@@ -62,7 +62,7 @@ class SquadronDef:
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, path: Path) -> SquadronDef:
|
||||
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
|
||||
from game.ato.ai_flight_planner_db import tasks_for_aircraft
|
||||
from game.ato import FlightType
|
||||
|
||||
with path.open(encoding="utf8") as squadron_file:
|
||||
|
||||
@@ -27,13 +27,12 @@ from dcs.ships import Forrestal, KUZNECOW, LHA_Tarawa, Stennis, Type_071
|
||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unitgroup import ShipGroup, StaticGroup
|
||||
|
||||
from game.dcs.helpers import unit_type_from_name
|
||||
from game.ground_forces.combat_stance import CombatStance
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.runways import RunwayAssigner, RunwayData
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.utils import Heading
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from gen.runways import RunwayAssigner, RunwayData
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from .base import Base
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import (
|
||||
|
||||
@@ -4,21 +4,20 @@ import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
import dcs.statics
|
||||
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.naming import namegen
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.theater import PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
)
|
||||
from .theatergroup import SceneryUnit, TheaterGroup
|
||||
from game.utils import Heading
|
||||
from game.version import VERSION
|
||||
from gen.naming import namegen
|
||||
from . import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
@@ -26,10 +25,11 @@ from . import (
|
||||
Fob,
|
||||
OffMapSpawn,
|
||||
)
|
||||
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
||||
from ..data.groups import GroupRole, GroupTask
|
||||
from ..armedforces.forcegroup import ForceGroup
|
||||
from .theatergroup import SceneryUnit, TheaterGroup
|
||||
from ..armedforces.armedforces import ArmedForces
|
||||
from ..armedforces.forcegroup import ForceGroup
|
||||
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
||||
from ..data.groups import GroupTask
|
||||
from ..profiling import logged_duration
|
||||
from ..settings import Settings
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import singledispatchmethod
|
||||
from typing import Optional, TYPE_CHECKING, Union, Iterable, Any
|
||||
from typing import Optional, TYPE_CHECKING, Union, Iterable
|
||||
|
||||
from dcs.mapping import Point as DcsPoint
|
||||
from shapely.geometry import (
|
||||
@@ -16,7 +16,7 @@ from shapely.ops import nearest_points, unary_union
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import ControlPoint, MissionTarget, TheaterGroundObject
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.ato import Flight, FlightWaypoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -45,6 +45,7 @@ from game.ato.flighttype import FlightType
|
||||
from game.ato.package import Package
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.naming import namegen
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.theater.transitnetwork import (
|
||||
@@ -52,10 +53,9 @@ from game.theater.transitnetwork import (
|
||||
TransitNetwork,
|
||||
)
|
||||
from game.utils import meters, nautical_miles
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.naming import namegen
|
||||
from game.ato.ai_flight_planner_db import aircraft_for_task
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.ato.flightplan import FlightPlanBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
Reference in New Issue
Block a user