Merge branch 'develop' into helipads

# Conflicts:
#	game/game.py
#	game/operation/operation.py
#	game/theater/controlpoint.py
#	gen/aircraft.py
#	resources/campaigns/golan_heights_lite.miz
This commit is contained in:
Khopa
2021-06-20 18:07:50 +02:00
635 changed files with 9708 additions and 8046 deletions

View File

@@ -1,22 +0,0 @@
from dcs.vehicles import AirDefence
AAA_UNITS = [
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.AAA_ZU_23_Closed_Emplacement,
AirDefence.AAA_ZU_23_Emplacement,
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement,
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_Emplacement,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_Flak_38_20mm,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
AirDefence.AAA_SP_Kdo_G_40,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_S_60_57mm,
]

View File

@@ -4,35 +4,35 @@ from dcs.vehicles import AirDefence
class AlicCodes:
CODES = {
AirDefence.EWR_1L13.id: 101,
AirDefence.EWR_55G6.id: 102,
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR.id: 103,
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id: 104,
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR.id: 107,
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR.id: 108,
AirDefence.MCC_SR_Sborka_Dog_Ear_SR.id: 109,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR.id: 110,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL.id: 115,
AirDefence.SAM_SA_8_Osa_Gecko_TEL.id: 117,
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL.id: 118,
AirDefence.SAM_SA_15_Tor_Gauntlet.id: 119,
AirDefence.SAM_SA_19_Tunguska_Grison.id: 120,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id: 121,
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3.id: 122,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR.id: 123,
AirDefence.SAM_Rapier_Blindfire_TR.id: 124,
AirDefence.SAM_Rapier_LN.id: 125,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR.id: 126,
AirDefence.HQ_7_Self_Propelled_LN.id: 127,
AirDefence.HQ_7_Self_Propelled_STR.id: 128,
AirDefence.SAM_Roland_ADS.id: 201,
AirDefence.SAM_Patriot_STR.id: 202,
AirDefence.SAM_Hawk_SR__AN_MPQ_50.id: 203,
AirDefence.SAM_Hawk_TR__AN_MPQ_46.id: 204,
AirDefence.SAM_Roland_EWR.id: 205,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55.id: 206,
AirDefence.SPAAA_Gepard.id: 207,
AirDefence.SPAAA_Vulcan_M163.id: 208,
AirDefence._1L13_EWR.id: 101,
AirDefence._55G6_EWR.id: 102,
AirDefence.S_300PS_40B6MD_sr.id: 103,
AirDefence.S_300PS_64H6E_sr.id: 104,
AirDefence.SA_11_Buk_SR_9S18M1.id: 107,
AirDefence.Kub_1S91_str.id: 108,
AirDefence.Dog_Ear_radar.id: 109,
AirDefence.S_300PS_40B6M_tr.id: 110,
AirDefence.SA_11_Buk_LN_9A310M1.id: 115,
AirDefence.Osa_9A33_ln.id: 117,
AirDefence.Strela_10M3.id: 118,
AirDefence.Tor_9A331.id: 119,
AirDefence._2S6_Tunguska.id: 120,
AirDefence.ZSU_23_4_Shilka.id: 121,
AirDefence.P_19_s_125_sr.id: 122,
AirDefence.Snr_s_125_tr.id: 123,
AirDefence.Rapier_fsa_blindfire_radar.id: 124,
AirDefence.Rapier_fsa_launcher.id: 125,
AirDefence.SNR_75V.id: 126,
AirDefence.HQ_7_LN_SP.id: 127,
AirDefence.HQ_7_STR_SP.id: 128,
AirDefence.Roland_ADS.id: 201,
AirDefence.Patriot_str.id: 202,
AirDefence.Hawk_sr.id: 203,
AirDefence.Hawk_tr.id: 204,
AirDefence.Roland_Radar.id: 205,
AirDefence.Hawk_cwar.id: 206,
AirDefence.Gepard.id: 207,
AirDefence.Vulcan.id: 208,
}
@classmethod

View File

@@ -1,239 +1,17 @@
from __future__ import annotations
from enum import unique, Enum
from typing import Type
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
from dcs.unittype import VehicleType
from pydcs_extensions.frenchpack import frenchpack
@unique
class GroundUnitClass(Enum):
Tank = (
"Tank",
(
Armor.MBT_T_55,
Armor.MBT_T_72B,
Armor.MBT_T_72B3,
Armor.MBT_T_80U,
Armor.MBT_T_90,
Armor.MBT_Leopard_2A4,
Armor.MBT_Leopard_2A4_Trs,
Armor.MBT_Leopard_2A5,
Armor.MBT_Leopard_2A6M,
Armor.MBT_Leopard_1A3,
Armor.MBT_Leclerc,
Armor.MBT_Challenger_II,
Armor.MBT_Chieftain_Mk_3,
Armor.MBT_M1A2_Abrams,
Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_IV,
Armor.ZTZ_96B,
# WW2
# Axis
Armor.Tk_PzIV_H,
Armor.SPG_Sturmpanzer_IV_Brummbar,
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
# Allies
Armor.Tk_M4_Sherman,
Armor.CT_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
# Mods
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_30B2,
frenchpack.Leclerc_Serie_XXI,
),
)
Atgm = (
"ATGM",
(
Armor.ATGM_HMMWV,
Armor.ATGM_VAB_Mephisto,
Armor.ATGM_Stryker,
Armor.IFV_BMP_2,
# WW2 (Tank Destroyers)
# Axxis
Armor.SPG_StuG_III_Ausf__G,
Armor.SPG_StuG_IV,
Armor.SPG_Jagdpanzer_IV,
Armor.SPG_Jagdpanther_G1,
Armor.SPG_Sd_Kfz_184_Elefant,
# Allies
Armor.SPG_M10_GMC,
Armor.MT_M4A4_Sherman_Firefly,
# Mods
frenchpack.VBAE_CRAB_MMP,
frenchpack.VAB_MEPHISTO,
frenchpack.TRM_2000_PAMELA,
),
)
Ifv = (
"IFV",
(
Armor.IFV_BMP_3,
Armor.IFV_BMP_2,
Armor.IFV_BMP_1,
Armor.IFV_Marder,
Armor.IFV_Warrior,
Armor.SPG_Stryker_MGS,
Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1,
Armor.ZBD_04A,
# Mods
frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13,
),
)
Apc = (
"APC",
(
Armor.IFV_M1126_Stryker_ICV,
Armor.APC_M113,
Armor.APC_BTR_80,
Armor.IFV_BTR_82A,
Armor.APC_MTLB,
Armor.APC_AAV_7_Amphibious,
Armor.APC_TPz_Fuchs,
Armor.APC_BTR_RD,
# WW2
Armor.APC_M2A1_Halftrack,
Armor.APC_Sd_Kfz_251_Halftrack,
# Mods
frenchpack.VAB__50,
frenchpack.VBL__50,
frenchpack.VBL_AANF1,
),
)
Artillery = (
"Artillery",
(
Artillery.Grad_MRL_FDDM__FC,
Artillery.MLRS_9A52_Smerch_HE_300mm,
Artillery.SPH_2S1_Gvozdika_122mm,
Artillery.SPH_2S3_Akatsia_152mm,
Artillery.MLRS_BM_21_Grad_122mm,
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
Artillery.SPH_M109_Paladin_155mm,
Artillery.MLRS_M270_227mm,
Artillery.SPM_2S9_Nona_120mm_M,
Artillery.SPH_Dana_vz77_152mm,
Artillery.SPH_T155_Firtina_155mm,
Artillery.PLZ_05,
Artillery.SPH_2S19_Msta_152mm,
Artillery.MLRS_9A52_Smerch_CM_300mm,
# WW2
Artillery.SPG_M12_GMC_155mm,
),
)
Logistics = (
"Logistics",
(
Unarmed.Carrier_M30_Cargo,
Unarmed.Truck_M818_6x6,
Unarmed.Truck_KAMAZ_43101,
Unarmed.Truck_Ural_375,
Unarmed.Truck_GAZ_66,
Unarmed.Truck_GAZ_3307,
Unarmed.Truck_GAZ_3308,
Unarmed.Truck_Ural_4320_31_Arm_d,
Unarmed.Truck_Ural_4320T,
Unarmed.Truck_Opel_Blitz,
Unarmed.LUV_Kubelwagen_82,
Unarmed.Carrier_Sd_Kfz_7_Tractor,
Unarmed.LUV_Kettenrad,
Unarmed.Car_Willys_Jeep,
Unarmed.LUV_Land_Rover_109,
Unarmed.Truck_Land_Rover_101_FC,
# Mods
frenchpack.VBL,
frenchpack.VAB,
),
)
Recon = (
"Recon",
(
Armor.Scout_HMMWV,
Armor.Scout_Cobra,
Armor.LT_PT_76,
Armor.IFV_LAV_25,
Armor.Scout_BRDM_2,
# WW2
Armor.LT_Mk_VII_Tetrarch,
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.Car_M8_Greyhound_Armored,
Armor.Car_Daimler_Armored,
# Mods
frenchpack.ERC_90,
frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR,
),
)
Infantry = (
"Infantry",
(
Infantry.Insurgent_AK_74,
Infantry.Infantry_AK_74,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_M4_Georgia,
Infantry.Infantry_AK_74_Rus,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_RPG_16,
Infantry.Infantry_M249,
Infantry.Infantry_M4,
Infantry.Infantry_RPG,
),
)
Shorads = (
"SHORADS",
(
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
AirDefence.SPAAA_ZSU_57_2,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.SAM_Linebacker___Bradley_M6,
AirDefence.SAM_Chaparral_M48,
AirDefence.SAM_Avenger__Stinger,
AirDefence.SAM_Roland_ADS,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_S_60_57mm,
AirDefence.AAA_M1_37mm,
AirDefence.AAA_QF_3_7,
),
)
def __init__(
self, class_name: str, unit_list: tuple[Type[VehicleType], ...]
) -> None:
self.class_name = class_name
self.unit_list = unit_list
def __contains__(self, unit_type: Type[VehicleType]) -> bool:
return unit_type in self.unit_list
Tank = "Tank"
Atgm = "ATGM"
Ifv = "IFV"
Apc = "APC"
Artillery = "Artillery"
Logistics = "Logistics"
Recon = "Recon"
Infantry = "Infantry"
Shorads = "SHORADS"
Manpads = "MANPADS"

View File

@@ -1,108 +1,108 @@
from dcs.ships import (
Battlecruiser_1144_2_Pyotr_Velikiy,
Cruiser_1164_Moskva,
CVN_70_Carl_Vinson,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017,
Frigate_11540_Neustrashimy,
Corvette_1124_4_Grisha,
Frigate_1135M_Rezky,
Corvette_1241_1_Molniya,
LHA_1_Tarawa,
FFG_Oliver_Hazzard_Perry,
CG_Ticonderoga,
Type_052B_Destroyer,
Type_052C_Destroyer,
Type_054A_Frigate,
DDG_Arleigh_Burke_IIa,
PIOTR,
MOSCOW,
VINSON,
CVN_71,
CVN_72,
CVN_73,
Stennis,
KUZNECOW,
CV_1143_5,
NEUSTRASH,
ALBATROS,
REZKY,
MOLNIYA,
LHA_Tarawa,
PERRY,
TICONDEROG,
Type_052B,
Type_052C,
Type_054A,
USS_Arleigh_Burke_IIa,
)
from dcs.vehicles import AirDefence
TELARS = {
AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SAM_Roland_ADS,
AirDefence._2S6_Tunguska,
AirDefence.SA_11_Buk_SR_9S18M1,
AirDefence.Osa_9A33_ln,
AirDefence.Tor_9A331,
AirDefence.Roland_ADS,
}
TRACK_RADARS = {
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.SAM_Patriot_STR,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_Self_Propelled_STR,
AirDefence.Kub_1S91_str,
AirDefence.Snr_s_125_tr,
AirDefence.S_300PS_40B6M_tr,
AirDefence.Hawk_tr,
AirDefence.Patriot_str,
AirDefence.SNR_75V,
AirDefence.Rapier_fsa_blindfire_radar,
AirDefence.HQ_7_STR_SP,
}
LAUNCHER_TRACKER_PAIRS = {
AirDefence.SAM_SA_6_Kub_Gainful_TEL: AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence.SAM_SA_3_S_125_Goa_LN: AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.SAM_Patriot_LN: AirDefence.SAM_Patriot_STR,
AirDefence.SAM_SA_2_S_75_Guideline_LN: AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.SAM_Rapier_LN: AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_Self_Propelled_LN: AirDefence.HQ_7_Self_Propelled_STR,
AirDefence.Kub_2P25_ln: AirDefence.Kub_1S91_str,
AirDefence._5p73_s_125_ln: AirDefence.Snr_s_125_tr,
AirDefence.S_300PS_5P85C_ln: AirDefence.S_300PS_40B6M_tr,
AirDefence.S_300PS_5P85D_ln: AirDefence.S_300PS_40B6M_tr,
AirDefence.Hawk_ln: AirDefence.Hawk_tr,
AirDefence.Patriot_ln: AirDefence.Patriot_str,
AirDefence.S_75M_Volhov: AirDefence.SNR_75V,
AirDefence.Rapier_fsa_launcher: AirDefence.Rapier_fsa_blindfire_radar,
AirDefence.HQ_7_LN_SP: AirDefence.HQ_7_STR_SP,
}
UNITS_WITH_RADAR = {
# Radars
AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.SAM_Roland_ADS,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.EWR_1L13,
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR,
AirDefence.EWR_55G6,
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR,
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR,
AirDefence.MCC_SR_Sborka_Dog_Ear_SR,
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.SAM_Hawk_SR__AN_MPQ_50,
AirDefence.SAM_Patriot_STR,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
AirDefence.SAM_Roland_EWR,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.HQ_7_Self_Propelled_STR,
AirDefence.EWR_FuMG_401_Freya_LZ,
AirDefence.EWR_FuSe_65_Würzburg_Riese,
AirDefence._2S6_Tunguska,
AirDefence.SA_11_Buk_LN_9A310M1,
AirDefence.Osa_9A33_ln,
AirDefence.Tor_9A331,
AirDefence.Gepard,
AirDefence.Vulcan,
AirDefence.Roland_ADS,
AirDefence.ZSU_23_4_Shilka,
AirDefence._1L13_EWR,
AirDefence.Kub_1S91_str,
AirDefence.S_300PS_40B6M_tr,
AirDefence.S_300PS_40B6MD_sr,
AirDefence._55G6_EWR,
AirDefence.S_300PS_64H6E_sr,
AirDefence.SA_11_Buk_SR_9S18M1,
AirDefence.Dog_Ear_radar,
AirDefence.Hawk_tr,
AirDefence.Hawk_sr,
AirDefence.Patriot_str,
AirDefence.Hawk_cwar,
AirDefence.P_19_s_125_sr,
AirDefence.Roland_Radar,
AirDefence.Snr_s_125_tr,
AirDefence.SNR_75V,
AirDefence.Rapier_fsa_blindfire_radar,
AirDefence.HQ_7_LN_SP,
AirDefence.HQ_7_STR_SP,
AirDefence.FuMG_401,
AirDefence.FuSe_65,
# Ships
CVN_70_Carl_Vinson,
FFG_Oliver_Hazzard_Perry,
CG_Ticonderoga,
Corvette_1124_4_Grisha,
CV_1143_5_Admiral_Kuznetsov,
Corvette_1241_1_Molniya,
Cruiser_1164_Moskva,
Frigate_11540_Neustrashimy,
Battlecruiser_1144_2_Pyotr_Velikiy,
Frigate_1135M_Rezky,
CV_1143_5_Admiral_Kuznetsov_2017,
CVN_74_John_C__Stennis,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
DDG_Arleigh_Burke_IIa,
LHA_1_Tarawa,
Type_052B_Destroyer,
Type_054A_Frigate,
Type_052C_Destroyer,
VINSON,
PERRY,
TICONDEROG,
ALBATROS,
KUZNECOW,
MOLNIYA,
MOSCOW,
NEUSTRASH,
PIOTR,
REZKY,
CV_1143_5,
Stennis,
CVN_71,
CVN_72,
CVN_73,
USS_Arleigh_Burke_IIa,
LHA_Tarawa,
Type_052B,
Type_054A,
Type_052C,
}

View File

@@ -5,12 +5,12 @@ import inspect
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, Iterator, Optional, Set, Tuple, Type, Union, cast
from typing import Dict, Iterator, Optional, Set, Tuple, Union, cast
from dcs.unitgroup import FlyingGroup
from dcs.unittype import FlyingType
from dcs.weapons_data import Weapons, weapon_ids
from game.dcs.aircrafttype import AircraftType
PydcsWeapon = Dict[str, Union[int, str]]
PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
@@ -97,12 +97,12 @@ class Pylon:
yield weapon
@classmethod
def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon:
def for_aircraft(cls, aircraft: AircraftType, number: int) -> Pylon:
# In pydcs these are all arbitrary inner classes of the aircraft type.
# The only way to identify them is by their name.
pylons = [
v
for v in aircraft.__dict__.values()
for v in aircraft.dcs_unit_type.__dict__.values()
if inspect.isclass(v) and v.__name__.startswith("Pylon")
]
@@ -121,8 +121,8 @@ class Pylon:
return cls(number, allowed)
@classmethod
def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]:
for pylon in sorted(list(aircraft.pylons)):
def iter_pylons(cls, aircraft: AircraftType) -> Iterator[Pylon]:
for pylon in sorted(list(aircraft.dcs_unit_type.pylons)):
yield cls.for_aircraft(aircraft, pylon)

View File

@@ -1,144 +1,37 @@
import json
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import List, Optional, Type, Union
from typing import Optional, Type, Union
from dcs.countries import country_dict
from dcs.helicopters import (
AH_1W,
AH_64A,
AH_64D,
CH_47D,
CH_53E,
Ka_50,
Mi_24V,
Mi_26,
Mi_28N,
Mi_8MT,
OH_58D,
SA342L,
SA342M,
SA342Minigun,
SA342Mistral,
SH_60B,
UH_1H,
UH_60A,
helicopter_map,
)
from dcs.mapping import Point
# mypy can't resolve these if they're wildcard imports for some reason.
from dcs.planes import (
AJS37,
AV8BNA,
A_10A,
A_10C,
A_10C_2,
A_20G,
A_50,
An_26B,
An_30M,
B_17G,
B_1B,
B_52H,
Bf_109K_4,
C_101CC,
C_130,
C_17A,
E_3A,
E_2C,
FA_18C_hornet,
FW_190A8,
FW_190D9,
F_117A,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
F_16A,
F_16C_50,
F_4E,
F_5E_3,
F_86F_Sabre,
IL_76MD,
IL_78M,
JF_17,
J_11A,
Ju_88A4,
KC130,
KC_135,
KC135MPRS,
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,
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_24MR,
Su_25,
Su_25T,
Su_27,
Su_30,
Su_33,
Su_34,
Tornado_GR4,
Tornado_IDS,
Tu_160,
Tu_22M3,
Tu_95MS,
WingLoong_I,
Yak_40,
plane_map,
I_16,
Tu_142,
)
from dcs.ships import (
Boat_Armed_Hi_speed,
Bulker_Yakushev,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
CVN_74_John_C__Stennis,
CVN_75_Harry_S__Truman,
CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017,
Cargo_Ivanov,
LHA_1_Tarawa,
Tanker_Elnya_160,
ship_map,
Stennis,
KUZNECOW,
CVN_71,
CVN_75,
CVN_73,
CVN_72,
CV_1143_5,
)
from dcs.terrain.terrain import Airport
from dcs.unit import Ship, Unit, Vehicle
from dcs.unitgroup import ShipGroup, StaticGroup
from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.unittype import UnitType
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
vehicle_map,
)
@@ -154,8 +47,6 @@ from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.su57.su57 import Su_57
UNITINFOTEXT_PATH = Path("./resources/units/unit_info_text.json")
plane_map["A-4E-C"] = A_4E_C
plane_map["F-22A"] = F_22A
plane_map["MB-339PAN"] = MB_339PAN
@@ -344,374 +235,6 @@ For example, player accessible Hornet is called `FA_18C_hornet`, and MANPAD Igla
# to be cheap enough to repair with a single turn's income.
RUNWAY_REPAIR_COST = 100
"""
Prices for the aircraft.
This defines both price for the player (although only aircraft listed in CAP/CAS/Transport/Armor/AirDefense roles will be purchasable)
and prioritization for the enemy (i.e. less important bases will receive units with lower price)
"""
PRICES = {
# fighter
MiG_23MLD: 13,
Su_27: 18,
Su_33: 22,
MiG_29A: 18,
MiG_29S: 20,
MiG_29G: 18,
MiG_25PD: 20,
MiG_31: 30,
J_11A: 26,
JF_17: 20,
Su_30: 24,
Su_57: 40,
SpitfireLFMkIX: 14,
SpitfireLFMkIXCW: 14,
I_16: 10,
Bf_109K_4: 14,
FW_190D9: 16,
FW_190A8: 14,
A_20G: 22,
Ju_88A4: 24,
F_5E_3: 8,
MiG_15bis: 4,
MiG_19P: 6,
F_86F_Sabre: 4,
MiG_21Bis: 8,
F_4E: 10,
AJS37: 12,
C_101CC: 6,
A_4E_C: 8,
MB_339PAN: 6,
AV8BNA: 14,
M_2000C: 16,
Mirage_2000_5: 20,
FA_18C_hornet: 22,
F_15C: 22,
F_15E: 24,
F_16C_50: 20,
F_16A: 14,
F_14A_135_GR: 20,
F_14B: 24,
F_22A: 40,
Tornado_IDS: 20,
Tornado_GR4: 20,
JAS39Gripen: 26,
# bomber
Su_17M4: 10,
Su_25: 15,
Su_25T: 18,
L_39ZA: 10,
Su_34: 24,
Su_24M: 20,
Su_24MR: 24,
MiG_27K: 20,
A_10A: 16,
A_10C: 22,
A_10C_2: 24,
S_3B: 10,
JAS39Gripen_AG: 26,
# heli
Ka_50: 13,
SA342M: 8,
SA342L: 5,
SA342Minigun: 4,
SA342Mistral: 8,
UH_1H: 4,
Mi_8MT: 5,
Mi_24V: 18,
Mi_28N: 24,
AH_1W: 20,
AH_64A: 24,
AH_64D: 30,
OH_58D: 6,
SH_60B: 6,
CH_47D: 4,
CH_53E: 4,
UH_60A: 4,
Mi_26: 4,
# Bombers
B_52H: 35,
B_1B: 50,
F_117A: 100,
Tu_160: 50,
Tu_22M3: 40,
Tu_95MS: 35,
Tu_142: 35,
# special
IL_76MD: 30,
An_26B: 25,
An_30M: 25,
Yak_40: 25,
S_3B_Tanker: 20,
IL_78M: 25,
KC_135: 25,
KC130: 25,
KC135MPRS: 25,
A_50: 50,
KJ_2000: 50,
E_3A: 50,
E_2C: 50,
C_130: 25,
Hercules: 25,
C_17A: 20,
# WW2
P_51D_30_NA: 18,
P_51D: 16,
P_47D_30: 17,
P_47D_30bl1: 16,
P_47D_40: 18,
B_17G: 30,
# Drones
MQ_9_Reaper: 12,
RQ_1A_Predator: 6,
WingLoong_I: 6,
# armor
Armor.APC_MTLB: 4,
Artillery.Grad_MRL_FDDM__FC: 4,
Armor.Scout_BRDM_2: 6,
Armor.APC_BTR_RD: 6,
Armor.APC_BTR_80: 8,
Armor.IFV_BTR_82A: 10,
Armor.MBT_T_55: 18,
Armor.MBT_T_72B: 20,
Armor.MBT_T_72B3: 25,
Armor.MBT_T_80U: 25,
Armor.MBT_T_90: 30,
Armor.IFV_BMD_1: 8,
Armor.IFV_BMP_1: 14,
Armor.IFV_BMP_2: 16,
Armor.IFV_BMP_3: 18,
Armor.LT_PT_76: 9,
Armor.ZBD_04A: 12,
Armor.ZTZ_96B: 30,
Armor.Scout_Cobra: 4,
Armor.APC_M113: 6,
Armor.Scout_HMMWV: 2,
Armor.ATGM_HMMWV: 8,
Armor.ATGM_VAB_Mephisto: 12,
Armor.IFV_M2A2_Bradley: 12,
Armor.IFV_M1126_Stryker_ICV: 10,
Armor.SPG_Stryker_MGS: 14,
Armor.ATGM_Stryker: 12,
Armor.MBT_M60A3_Patton: 16,
Armor.MBT_M1A2_Abrams: 25,
Armor.MBT_Leclerc: 25,
Armor.MBT_Leopard_1A3: 18,
Armor.MBT_Leopard_2A4: 20,
Armor.MBT_Leopard_2A4_Trs: 20,
Armor.MBT_Leopard_2A5: 22,
Armor.MBT_Leopard_2A6M: 25,
Armor.MBT_Merkava_IV: 25,
Armor.APC_TPz_Fuchs: 5,
Armor.MBT_Challenger_II: 25,
Armor.MBT_Chieftain_Mk_3: 20,
Armor.IFV_Marder: 10,
Armor.IFV_Warrior: 10,
Armor.IFV_LAV_25: 7,
Artillery.MLRS_M270_227mm: 55,
Artillery.SPH_M109_Paladin_155mm: 25,
Artillery.SPM_2S9_Nona_120mm_M: 12,
Artillery.SPH_2S1_Gvozdika_122mm: 18,
Artillery.SPH_2S3_Akatsia_152mm: 24,
Artillery.SPH_2S19_Msta_152mm: 30,
Artillery.MLRS_BM_21_Grad_122mm: 15,
Artillery.MLRS_9K57_Uragan_BM_27_220mm: 50,
Artillery.MLRS_9A52_Smerch_HE_300mm: 40,
Artillery.Mortar_2B11_120mm: 4,
Artillery.SPH_Dana_vz77_152mm: 26,
Artillery.PLZ_05: 25,
Artillery.SPH_T155_Firtina_155mm: 28,
Unarmed.LUV_UAZ_469_Jeep: 3,
Unarmed.Truck_Ural_375: 3,
Infantry.Infantry_M4: 1,
Infantry.Infantry_AK_74: 1,
Unarmed.Truck_M818_6x6: 3,
# WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G: 24,
Armor.Tk_PzIV_H: 16,
Armor.HT_Pz_Kpfw_VI_Tiger_I: 24,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II: 26,
Armor.SPG_Jagdpanther_G1: 18,
Armor.SPG_Jagdpanzer_IV: 11,
Armor.SPG_Sd_Kfz_184_Elefant: 18,
Armor.APC_Sd_Kfz_251_Halftrack: 4,
Armor.IFV_Sd_Kfz_234_2_Puma: 8,
Armor.Tk_M4_Sherman: 12,
Armor.MT_M4A4_Sherman_Firefly: 16,
Armor.CT_Cromwell_IV: 12,
Unarmed.Carrier_M30_Cargo: 2,
Armor.APC_M2A1_Halftrack: 4,
Armor.CT_Centaur_IV: 10,
Armor.HIT_Churchill_VII: 16,
Armor.Car_M8_Greyhound_Armored: 8,
Armor.SPG_M10_GMC: 14,
Armor.SPG_StuG_III_Ausf__G: 12,
Armor.SPG_StuG_IV: 14,
Artillery.SPG_M12_GMC_155mm: 10,
Armor.SPG_Sturmpanzer_IV_Brummbar: 10,
Armor.Car_Daimler_Armored: 8,
Armor.LT_Mk_VII_Tetrarch: 8,
Unarmed.Tractor_M4_Hi_Speed: 2,
Unarmed.Carrier_Sd_Kfz_7_Tractor: 1,
Unarmed.LUV_Kettenrad: 1,
Unarmed.LUV_Kubelwagen_82: 1,
Unarmed.Truck_Opel_Blitz: 1,
Unarmed.Truck_Bedford: 1,
Unarmed.Truck_GMC_Jimmy_6x6_Truck: 1,
# ship
CV_1143_5_Admiral_Kuznetsov: 100,
CVN_74_John_C__Stennis: 100,
LHA_1_Tarawa: 50,
Bulker_Yakushev: 10,
Boat_Armed_Hi_speed: 10,
Cargo_Ivanov: 10,
Tanker_Elnya_160: 10,
# Air Defence units
AirDefence.SAM_SA_19_Tunguska_Grison: 30,
AirDefence.SAM_SA_6_Kub_Gainful_TEL: 20,
AirDefence.SAM_SA_3_S_125_Goa_LN: 6,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL: 30,
AirDefence.SAM_SA_11_Buk_Gadfly_C2: 25,
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR: 28,
AirDefence.SAM_SA_8_Osa_Gecko_TEL: 28,
AirDefence.SAM_SA_15_Tor_Gauntlet: 40,
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL: 16,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL: 12,
AirDefence.SAM_SA_8_Osa_LD_9T217: 22,
AirDefence.SAM_Patriot_CR__AMG_AN_MRC_137: 35,
AirDefence.SAM_Patriot_ECS: 30,
AirDefence.SPAAA_Gepard: 24,
AirDefence.SAM_Hawk_Platoon_Command_Post__PCP: 14,
AirDefence.SPAAA_Vulcan_M163: 10,
AirDefence.SAM_Hawk_LN_M192: 8,
AirDefence.SAM_Chaparral_M48: 16,
AirDefence.SAM_Linebacker___Bradley_M6: 18,
AirDefence.SAM_Patriot_LN: 15,
AirDefence.SAM_Avenger__Stinger: 20,
AirDefence.SAM_Patriot_EPP_III: 15,
AirDefence.SAM_Patriot_C2_ICC: 18,
AirDefence.SAM_Roland_ADS: 12,
AirDefence.MANPADS_Stinger: 6,
AirDefence.MANPADS_Stinger_C2_Desert: 4,
AirDefence.MANPADS_Stinger_C2: 4,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish: 10,
AirDefence.SPAAA_ZSU_57_2: 12,
AirDefence.AAA_ZU_23_Closed_Emplacement: 6,
AirDefence.AAA_ZU_23_Emplacement: 6,
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375: 7,
AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement: 6,
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375: 7,
AirDefence.AAA_ZU_23_Insurgent_Emplacement: 6,
AirDefence.MANPADS_SA_18_Igla_Grouse: 10,
AirDefence.MANPADS_SA_18_Igla_Grouse_C2: 8,
AirDefence.MANPADS_SA_18_Igla_S_Grouse: 12,
AirDefence.MANPADS_SA_18_Igla_S_Grouse_C2: 8,
AirDefence.EWR_1L13: 30,
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR: 22,
AirDefence.EWR_55G6: 30,
AirDefence.MCC_SR_Sborka_Dog_Ear_SR: 10,
AirDefence.SAM_Hawk_TR__AN_MPQ_46: 14,
AirDefence.SAM_Hawk_SR__AN_MPQ_50: 18,
AirDefence.SAM_Patriot_STR: 22,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55: 20,
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3: 14,
AirDefence.SAM_Roland_EWR: 16,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR: 14,
AirDefence.SAM_SA_2_S_75_Guideline_LN: 8,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR: 12,
AirDefence.SAM_Rapier_LN: 6,
AirDefence.SAM_Rapier_Tracker: 6,
AirDefence.SAM_Rapier_Blindfire_TR: 8,
AirDefence.HQ_7_Self_Propelled_LN: 20,
AirDefence.HQ_7_Self_Propelled_STR: 24,
AirDefence.AAA_8_8cm_Flak_18: 6,
AirDefence.AAA_Flak_38_20mm: 6,
AirDefence.AAA_8_8cm_Flak_36: 8,
AirDefence.AAA_8_8cm_Flak_37: 9,
AirDefence.AAA_Flak_Vierling_38_Quad_20mm: 5,
AirDefence.AAA_SP_Kdo_G_40: 8,
AirDefence.SL_Flakscheinwerfer_37: 4,
AirDefence.PU_Maschinensatz_33: 10,
AirDefence.AAA_8_8cm_Flak_41: 10,
AirDefence.EWR_FuMG_401_Freya_LZ: 25,
AirDefence.AAA_Bofors_40mm: 8,
AirDefence.AAA_S_60_57mm: 8,
AirDefence.AAA_M1_37mm: 7,
AirDefence.AAA_M45_Quadmount_HB_12_7mm: 4,
AirDefence.AAA_QF_3_7: 10,
# FRENCH PACK MOD
frenchpack.AMX_10RCR: 10,
frenchpack.AMX_10RCR_SEPAR: 12,
frenchpack.ERC_90: 12,
frenchpack.MO_120_RT: 10,
frenchpack._53T2: 4,
frenchpack.TRM_2000: 4,
frenchpack.TRM_2000_Fuel: 4,
frenchpack.TRM_2000_53T2: 8,
frenchpack.TRM_2000_PAMELA: 14,
frenchpack.VAB_MEDICAL: 8,
frenchpack.VAB: 6,
frenchpack.VAB__50: 4,
frenchpack.VAB_T20_13: 6,
frenchpack.VAB_MEPHISTO: 8,
frenchpack.VAB_MORTIER: 10,
frenchpack.VBL__50: 4,
frenchpack.VBL_AANF1: 2,
frenchpack.VBL: 1,
frenchpack.VBAE_CRAB: 8,
frenchpack.VBAE_CRAB_MMP: 12,
frenchpack.AMX_30B2: 18,
frenchpack.Tracma_TD_1500: 2,
frenchpack.Infantry_Soldier_JTAC: 1,
frenchpack.Leclerc_Serie_XXI: 35,
frenchpack.DIM__TOYOTA_BLUE: 2,
frenchpack.DIM__TOYOTA_GREEN: 2,
frenchpack.DIM__TOYOTA_DESERT: 2,
frenchpack.DIM__KAMIKAZE: 6,
# SA-10
AirDefence.SAM_SA_10_S_300_Grumble_C2: 18,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR: 24,
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR: 30,
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR: 30,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: 22,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D: 22,
# High digit sams mod
highdigitsams.AAA_SON_9_Fire_Can: 8,
highdigitsams.AAA_100mm_KS_19: 10,
highdigitsams.SAM_SA_10B_S_300PS_54K6_CP: 20,
highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN: 24,
highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN: 24,
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE: 24,
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE: 24,
highdigitsams.SAM_SA_10B_S_300PS_30N6_TR: 26,
highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR: 26,
highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR: 32,
highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR: 32,
highdigitsams.SAM_SA_12_S_300V_9S457_CP: 22,
highdigitsams.SAM_SA_12_S_300V_9A82_LN: 26,
highdigitsams.SAM_SA_12_S_300V_9A83_LN: 26,
highdigitsams.SAM_SA_12_S_300V_9S15_SR: 34,
highdigitsams.SAM_SA_12_S_300V_9S19_SR: 34,
highdigitsams.SAM_SA_12_S_300V_9S32_TR: 28,
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6: 26,
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E: 30,
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck: 32,
highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E: 38,
highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E: 38,
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE: 28,
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE: 28,
highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2: 27,
highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck: 33,
highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2: 40,
highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2: 30,
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP: 30,
highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR: 45,
highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR: 45,
highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR: 35,
highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN: 32,
highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN: 32,
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2: 40,
}
"""
Units separated by country.
country : DCS Country name
@@ -790,44 +313,6 @@ REWARDS = {
"derrick": 8,
}
CARRIER_CAPABLE = [
FA_18C_hornet,
F_14A_135_GR,
F_14B,
AV8BNA,
Su_33,
A_4E_C,
S_3B,
S_3B_Tanker,
E_2C,
UH_1H,
Mi_8MT,
Ka_50,
AH_1W,
OH_58D,
UH_60A,
SH_60B,
SA342L,
SA342M,
SA342Minigun,
SA342Mistral,
]
LHA_CAPABLE = [
AV8BNA,
UH_1H,
Mi_8MT,
Ka_50,
AH_1W,
OH_58D,
UH_60A,
SH_60B,
SA342L,
SA342M,
SA342Minigun,
SA342Mistral,
]
"""
---------- END OF CONFIGURATION SECTION
"""
@@ -836,124 +321,25 @@ StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
def upgrade_to_supercarrier(unit, name: str):
if unit == CVN_74_John_C__Stennis:
if unit == Stennis:
if name == "CVN-71 Theodore Roosevelt":
return CVN_71_Theodore_Roosevelt
return CVN_71
elif name == "CVN-72 Abraham Lincoln":
return CVN_72_Abraham_Lincoln
return CVN_72
elif name == "CVN-73 George Washington":
return CVN_73_George_Washington
return CVN_73
elif name == "CVN-75 Harry S. Truman":
return CVN_75_Harry_S__Truman
return CVN_75
elif name == "Carrier Strike Group 8":
return CVN_75_Harry_S__Truman
return CVN_75
else:
return CVN_71_Theodore_Roosevelt
elif unit == CV_1143_5_Admiral_Kuznetsov:
return CV_1143_5_Admiral_Kuznetsov_2017
return CVN_71
elif unit == KUZNECOW:
return CV_1143_5
else:
return unit
MANPADS: List[Type[VehicleType]] = [
AirDefence.MANPADS_SA_18_Igla_Grouse,
AirDefence.MANPADS_SA_18_Igla_S_Grouse,
AirDefence.MANPADS_Stinger,
]
INFANTRY: List[VehicleType] = [
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS,
Infantry.Infantry_RPG,
Infantry.Infantry_M4,
Infantry.Infantry_M4,
Infantry.Infantry_M4,
Infantry.Infantry_M4,
Infantry.Infantry_M4,
Infantry.Infantry_M249,
Artillery.Mortar_2B11_120mm,
Infantry.Infantry_AK_74,
Infantry.Infantry_AK_74,
Infantry.Infantry_AK_74,
Infantry.Infantry_AK_74,
Infantry.Infantry_AK_74,
Infantry.Paratrooper_RPG_16,
Infantry.Infantry_M4_Georgia,
Infantry.Infantry_M4_Georgia,
Infantry.Infantry_M4_Georgia,
Infantry.Infantry_M4_Georgia,
Infantry.Infantry_AK_74_Rus,
Infantry.Infantry_AK_74_Rus,
Infantry.Infantry_AK_74_Rus,
Infantry.Infantry_AK_74_Rus,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_M1_Garand,
Infantry.Insurgent_AK_74,
Infantry.Insurgent_AK_74,
Infantry.Insurgent_AK_74,
]
def find_manpad(country_name: str) -> List[VehicleType]:
return [x for x in MANPADS if x in FACTIONS[country_name].infantry_units]
def find_infantry(country_name: str, allow_manpad: bool = False) -> List[VehicleType]:
if allow_manpad:
inf = INFANTRY + MANPADS
else:
inf = INFANTRY
return [x for x in inf if x in FACTIONS[country_name].infantry_units]
def unit_type_name(unit_type) -> str:
return unit_type.id and unit_type.id or unit_type.name
def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id
def unit_get_expanded_info(country_name: str, unit_type, request_type: str) -> str:
original_name = unit_type.name and unit_type.name or unit_type.id
default_value = None
faction_value = None
with UNITINFOTEXT_PATH.open("r", encoding="utf-8") as fdata:
data = json.load(fdata)
type_exists = data.get(original_name)
if type_exists is not None:
for faction in type_exists:
if default_value is None:
default_exists = faction.get("default")
if default_exists is not None:
default_value = default_exists.get(request_type)
if faction_value is None:
faction_exists = faction.get(country_name)
if faction_exists is not None:
faction_value = faction_exists.get(request_type)
if default_value is None:
if request_type == "text":
return "WIP - This unit doesn't have any description text yet."
if request_type == "name":
return original_name
else:
return "Unknown"
if faction_value is None:
return default_value
return faction_value
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
if name in vehicle_map:
return vehicle_map[name]
@@ -967,22 +353,6 @@ def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
return None
def flying_type_from_name(name: str) -> Optional[Type[FlyingType]]:
unit_type = plane_map.get(name)
if unit_type is not None:
return unit_type
return helicopter_map.get(name)
def unit_type_of(unit: Unit) -> UnitType:
if isinstance(unit, Vehicle):
return vehicle_map[unit.type]
elif isinstance(unit, Ship):
return ship_map[unit.type]
else:
return unit.type
def country_id_from_name(name):
for k, v in country_dict.items():
if v.name == name:
@@ -1000,39 +370,3 @@ F_16C_50.Liveries = DefaultLiveries
P_51D_30_NA.Liveries = DefaultLiveries
Ju_88A4.Liveries = DefaultLiveries
B_17G.Liveries = DefaultLiveries
# List of airframes that rely on their gun as a primary weapon. We confiscate bullets
# from most AI air-to-ground missions since they aren't smart enough to RTB when they're
# out of everything other than bullets (DCS does not have an all-but-gun winchester
# option) and we don't want to be attacking fully functional Tors with a Vulcan.
#
# These airframes are the exceptions. They probably should be using their gun regardless
# of the mission type.
GUN_RELIANT_AIRFRAMES: List[Type[FlyingType]] = [
AH_1W,
AH_64A,
AH_64D,
A_10A,
A_10C,
A_10C_2,
A_20G,
Bf_109K_4,
FW_190A8,
FW_190D9,
F_86F_Sabre,
Ju_88A4,
Ka_50,
MiG_15bis,
MiG_19P,
Mi_24V,
Mi_28N,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_25,
Su_25T,
]

212
game/dcs/aircrafttype.py Normal file
View File

@@ -0,0 +1,212 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import ClassVar, Type, Iterator, TYPE_CHECKING, Optional, Any
import yaml
from dcs.helicopters import helicopter_map
from dcs.planes import plane_map
from dcs.unittype import FlyingType
from game.dcs.unittype import UnitType
from game.radio.channels import (
ChannelNamer,
RadioChannelAllocator,
CommonRadioChannelAllocator,
HueyChannelNamer,
SCR522ChannelNamer,
ViggenChannelNamer,
ViperChannelNamer,
TomcatChannelNamer,
MirageChannelNamer,
SingleRadioChannelNamer,
FarmerRadioChannelAllocator,
SCR522RadioChannelAllocator,
ViggenRadioChannelAllocator,
NoOpChannelAllocator,
)
from game.utils import Speed, kph
if TYPE_CHECKING:
from gen.aircraft import FlightData
from gen import AirSupport, RadioFrequency, RadioRegistry
from gen.radios import Radio
@dataclass(frozen=True)
class RadioConfig:
inter_flight: Optional[Radio]
intra_flight: Optional[Radio]
channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer]
@classmethod
def from_data(cls, data: dict[str, Any]) -> RadioConfig:
return RadioConfig(
cls.make_radio(data.get("inter_flight", None)),
cls.make_radio(data.get("intra_flight", None)),
cls.make_allocator(data.get("channels", {})),
cls.make_namer(data.get("channels", {})),
)
@classmethod
def make_radio(cls, name: Optional[str]) -> Optional[Radio]:
from gen.radios import get_radio
if name is None:
return None
return get_radio(name)
@classmethod
def make_allocator(cls, data: dict[str, Any]) -> Optional[RadioChannelAllocator]:
try:
alloc_type = data["type"]
except KeyError:
return None
allocator_type: Type[RadioChannelAllocator] = {
"SCR-522": SCR522RadioChannelAllocator,
"common": CommonRadioChannelAllocator,
"farmer": FarmerRadioChannelAllocator,
"noop": NoOpChannelAllocator,
"viggen": ViggenRadioChannelAllocator,
}[alloc_type]
return allocator_type.from_cfg(data)
@classmethod
def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]:
return {
"SCR-522": SCR522ChannelNamer,
"default": ChannelNamer,
"huey": HueyChannelNamer,
"mirage": MirageChannelNamer,
"single": SingleRadioChannelNamer,
"tomcat": TomcatChannelNamer,
"viggen": ViggenChannelNamer,
"viper": ViperChannelNamer,
}[config.get("namer", "default")]
@dataclass(frozen=True)
class AircraftType(UnitType[FlyingType]):
carrier_capable: bool
lha_capable: bool
always_keeps_gun: bool
intra_flight_radio: Optional[Radio]
channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer]
_by_name: ClassVar[dict[str, AircraftType]] = {}
_by_unit_type: ClassVar[dict[Type[FlyingType], list[AircraftType]]] = defaultdict(
list
)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@property
def flyable(self) -> bool:
return self.dcs_unit_type.flyable
@cached_property
def max_speed(self) -> Speed:
return kph(self.dcs_unit_type.max_speed)
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
from gen.radios import ChannelInUseError, MHz
if self.intra_flight_radio is not None:
return radio_registry.alloc_for_radio(self.intra_flight_radio)
freq = MHz(self.dcs_unit_type.radio_frequency)
try:
radio_registry.reserve(freq)
except ChannelInUseError:
pass
return freq
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
if self.channel_allocator is not None:
self.channel_allocator.assign_channels_for_flight(flight, air_support)
def channel_name(self, radio_id: int, channel_id: int) -> str:
return self.channel_namer.channel_name(radio_id, channel_id)
@classmethod
def register(cls, aircraft_type: AircraftType) -> None:
cls._by_name[aircraft_type.name] = aircraft_type
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
@classmethod
def named(cls, name: str) -> AircraftType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
yield from cls._by_unit_type[dcs_unit_type]
@staticmethod
def _each_unit_type() -> Iterator[Type[FlyingType]]:
yield from helicopter_map.values()
yield from plane_map.values()
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@classmethod
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {aircraft.id}; it will not be available")
return
with data_path.open() as data_file:
data = yaml.safe_load(data_file)
try:
price = data["price"]
except KeyError as ex:
raise KeyError(f"Missing required price field: {data_path}") from ex
radio_config = RadioConfig.from_data(data.get("radios", {}))
try:
introduction = data["introduced"]
if introduction is None:
introduction = "N/A"
except KeyError:
introduction = "No data."
for variant in data.get("variants", [aircraft.id]):
yield AircraftType(
dcs_unit_type=aircraft,
name=variant,
description=data.get("description", "No data."),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=price,
carrier_capable=data.get("carrier_capable", False),
lha_capable=data.get("lha_capable", False),
always_keeps_gun=data.get("always_keeps_gun", False),
intra_flight_radio=radio_config.intra_flight,
channel_allocator=radio_config.channel_allocator,
channel_namer=radio_config.channel_namer,
)

View File

@@ -0,0 +1,95 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Type, Optional, ClassVar, Iterator
import yaml
from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map
from game.data.groundunitclass import GroundUnitClass
from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class GroundUnitType(UnitType[VehicleType]):
unit_class: Optional[GroundUnitClass]
spawn_weight: int
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
_by_unit_type: ClassVar[
dict[Type[VehicleType], list[GroundUnitType]]
] = defaultdict(list)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@classmethod
def register(cls, aircraft_type: GroundUnitType) -> None:
cls._by_name[aircraft_type.name] = aircraft_type
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
@classmethod
def named(cls, name: str) -> GroundUnitType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
yield from cls._by_unit_type[dcs_unit_type]
@staticmethod
def _each_unit_type() -> Iterator[Type[VehicleType]]:
yield from vehicle_map.values()
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@classmethod
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {vehicle.id}; it will not be available")
return
with data_path.open() as data_file:
data = yaml.safe_load(data_file)
try:
introduction = data["introduced"]
if introduction is None:
introduction = "N/A"
except KeyError:
introduction = "No data."
class_name = data.get("class")
unit_class: Optional[GroundUnitClass] = None
if class_name is not None:
unit_class = GroundUnitClass(class_name)
for variant in data.get("variants", [vehicle.id]):
yield GroundUnitType(
dcs_unit_type=vehicle,
unit_class=unit_class,
spawn_weight=data.get("spawn_weight", 0),
name=variant,
description=data.get("description", "No data."),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=data.get("price", 1),
)

26
game/dcs/unittype.py Normal file
View File

@@ -0,0 +1,26 @@
from dataclasses import dataclass
from functools import cached_property
from typing import TypeVar, Generic, Type
from dcs.unittype import UnitType as DcsUnitType
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=DcsUnitType)
@dataclass(frozen=True)
class UnitType(Generic[DcsUnitTypeT]):
dcs_unit_type: Type[DcsUnitTypeT]
name: str
description: str
year_introduced: str
country_of_origin: str
manufacturer: str
role: str
price: int
def __str__(self) -> str:
return self.name
@cached_property
def eplrs_capable(self) -> bool:
return getattr(self.dcs_unit_type, "eplrs", False)

View File

@@ -14,13 +14,12 @@ from typing import (
Dict,
Iterator,
List,
Type,
TYPE_CHECKING,
)
from dcs.unittype import FlyingType, UnitType
from game import db
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater import Airfield, ControlPoint
from game.transfers import CargoShip
from game.unitmap import (
@@ -49,8 +48,8 @@ class AirLosses:
def losses(self) -> Iterator[FlyingUnit]:
return itertools.chain(self.player, self.enemy)
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
def by_type(self, player: bool) -> Dict[AircraftType, int]:
losses_by_type: Dict[AircraftType, int] = defaultdict(int)
losses = self.player if player else self.enemy
for loss in losses:
losses_by_type[loss.flight.unit_type] += 1
@@ -182,8 +181,8 @@ class Debriefing:
def casualty_count(self, control_point: ControlPoint) -> int:
return len([x for x in self.front_line_losses if x.origin == control_point])
def front_line_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
losses = self.ground_losses.player_front_line
else:
@@ -192,8 +191,8 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1
return losses_by_type
def convoy_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
losses = self.ground_losses.player_convoy
else:
@@ -202,8 +201,8 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1
return losses_by_type
def cargo_ship_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
def cargo_ship_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
ships = self.ground_losses.player_cargo_ships
else:
@@ -213,8 +212,8 @@ class Debriefing:
losses_by_type[unit_type] += count
return losses_by_type
def airlift_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
def airlift_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
losses = self.ground_losses.player_airlifts
else:

View File

@@ -14,6 +14,7 @@ from game.operation.operation import Operation
from game.theater import ControlPoint
from gen import AirTaskingOrder
from gen.ground_forces.combat_stance import CombatStance
from ..dcs.groundunittype import GroundUnitType
from ..unitmap import UnitMap
if TYPE_CHECKING:
@@ -122,7 +123,7 @@ class Event:
def commit_air_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.air_losses.losses:
if (
if loss.pilot is not None and (
not loss.pilot.player
or not self.game.settings.invulnerable_player_pilots
):
@@ -439,7 +440,7 @@ class Event:
# Also transfer pending deliveries.
for unit_type, count in source.pending_unit_deliveries.units.items():
if not issubclass(unit_type, VehicleType):
if not isinstance(unit_type, GroundUnitType):
continue
if count <= 0:
# Don't transfer *sales*...

View File

@@ -1,15 +1,13 @@
from __future__ import annotations
from game.data.groundunitclass import GroundUnitClass
import itertools
import logging
from dataclasses import dataclass, field
from typing import Optional, Dict, Type, List, Any, cast, Iterator
from typing import Optional, Dict, Type, List, Any, Iterator
import dcs
from dcs.countries import country_dict
from dcs.planes import plane_map
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
from dcs.unittype import ShipType, UnitType
from game.data.building_data import (
WW2_ALLIES_BUILDINGS,
@@ -23,7 +21,9 @@ from game.data.doctrine import (
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
)
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
from game.data.groundunitclass import GroundUnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
@dataclass
@@ -45,25 +45,25 @@ class Faction:
description: str = field(default="")
# Available aircraft
aircrafts: List[Type[FlyingType]] = field(default_factory=list)
aircrafts: List[AircraftType] = field(default_factory=list)
# Available awacs aircraft
awacs: List[Type[FlyingType]] = field(default_factory=list)
awacs: List[AircraftType] = field(default_factory=list)
# Available tanker aircraft
tankers: List[Type[FlyingType]] = field(default_factory=list)
tankers: List[AircraftType] = field(default_factory=list)
# Available frontline units
frontline_units: List[Type[VehicleType]] = field(default_factory=list)
frontline_units: List[GroundUnitType] = field(default_factory=list)
# Available artillery units
artillery_units: List[Type[VehicleType]] = field(default_factory=list)
artillery_units: List[GroundUnitType] = field(default_factory=list)
# Infantry units used
infantry_units: List[Type[VehicleType]] = field(default_factory=list)
infantry_units: List[GroundUnitType] = field(default_factory=list)
# Logistics units used
logistics_units: List[Type[VehicleType]] = field(default_factory=list)
logistics_units: List[GroundUnitType] = field(default_factory=list)
# Possible SAMS site generators for this faction
air_defenses: List[str] = field(default_factory=list)
@@ -114,7 +114,7 @@ class Faction:
has_jtac: bool = field(default=False)
# Unit to use as JTAC for this faction
jtac_unit: Optional[Type[FlyingType]] = field(default=None)
jtac_unit: Optional[AircraftType] = field(default=None)
# doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
@@ -123,7 +123,7 @@ class Faction:
building_set: List[str] = field(default_factory=list)
# List of default livery overrides
liveries_overrides: Dict[Type[UnitType], List[str]] = field(default_factory=dict)
liveries_overrides: Dict[AircraftType, List[str]] = field(default_factory=dict)
#: Set to True if the faction should force the "Unrestricted satnav" option
#: for the mission. This option enables GPS for capable aircraft regardless
@@ -134,15 +134,11 @@ class Faction:
#: both will use it.
unrestricted_satnav: bool = False
def has_access_to_unittype(self, unitclass: GroundUnitClass) -> bool:
has_access = False
for vehicle in unitclass.unit_list:
if vehicle in self.frontline_units:
def has_access_to_unittype(self, unit_class: GroundUnitClass) -> bool:
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
if vehicle.unit_class is unit_class:
return True
if vehicle in self.artillery_units:
return True
return has_access
return False
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@@ -163,18 +159,26 @@ class Faction:
faction.authors = json.get("authors", "")
faction.description = json.get("description", "")
faction.aircrafts = load_all_aircraft(json.get("aircrafts", []))
faction.awacs = load_all_aircraft(json.get("awacs", []))
faction.tankers = load_all_aircraft(json.get("tankers", []))
faction.aircrafts = [AircraftType.named(n) for n in json.get("aircrafts", [])]
faction.awacs = [AircraftType.named(n) for n in json.get("awacs", [])]
faction.tankers = [AircraftType.named(n) for n in json.get("tankers", [])]
faction.aircrafts = list(
set(faction.aircrafts + faction.awacs + faction.tankers)
)
faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
faction.infantry_units = load_all_vehicles(json.get("infantry_units", []))
faction.logistics_units = load_all_vehicles(json.get("logistics_units", []))
faction.frontline_units = [
GroundUnitType.named(n) for n in json.get("frontline_units", [])
]
faction.artillery_units = [
GroundUnitType.named(n) for n in json.get("artillery_units", [])
]
faction.infantry_units = [
GroundUnitType.named(n) for n in json.get("infantry_units", [])
]
faction.logistics_units = [
GroundUnitType.named(n) for n in json.get("logistics_units", [])
]
faction.ewrs = json.get("ewrs", [])
@@ -198,7 +202,7 @@ class Faction:
faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None)
if jtac_name is not None:
faction.jtac_unit = load_aircraft(jtac_name)
faction.jtac_unit = AircraftType.named(jtac_name)
else:
faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1))
@@ -232,93 +236,33 @@ class Faction:
# Load liveries override
faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {})
for k, v in liveries_overrides.items():
k = load_aircraft(k)
if k is not None:
faction.liveries_overrides[k] = [s.lower() for s in v]
for name, livery in liveries_overrides.items():
aircraft = AircraftType.named(name)
faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
return faction
@property
def all_units(self) -> List[Type[UnitType]]:
return (
self.infantry_units
+ self.aircrafts
+ self.awacs
+ self.artillery_units
+ self.frontline_units
+ self.tankers
+ self.logistics_units
)
@property
def ground_units(self) -> Iterator[Type[VehicleType]]:
def ground_units(self) -> Iterator[GroundUnitType]:
yield from self.artillery_units
yield from self.frontline_units
yield from self.logistics_units
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
"""
Find unit by name
:param unit: Unit name as string
:param class_repository: Repository of classes (Either a module, a class, or a list of classes)
:return: The unit as a PyDCS type
"""
if unit is None:
return None
elif unit in plane_map.keys():
return plane_map[unit]
else:
for mother_class in class_repository:
if getattr(mother_class, unit, None) is not None:
return getattr(mother_class, unit)
if type(mother_class) is list:
for m in mother_class:
if m.__name__ == unit:
return m
logging.error(f"FACTION ERROR : Unable to find {unit} in pydcs")
return None
def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
return cast(
Optional[FlyingType],
unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]),
)
def load_all_aircraft(data) -> List[Type[FlyingType]]:
items = []
for name in data:
item = load_aircraft(name)
if item is not None:
items.append(item)
return items
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
return cast(
Optional[FlyingType],
unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
),
)
def load_all_vehicles(data) -> List[Type[VehicleType]]:
items = []
for name in data:
item = load_vehicle(name)
if item is not None:
items.append(item)
return items
def infantry_with_class(
self, unit_class: GroundUnitClass
) -> Iterator[GroundUnitType]:
for unit in self.infantry_units:
if unit.unit_class is unit_class:
yield unit
def load_ship(name: str) -> Optional[Type[ShipType]]:
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
if (ship := getattr(dcs.ships, name, None)) is not None:
return ship
logging.error(f"FACTION ERROR : Unable to find {name} in dcs.ships")
return None
def load_all_ships(data) -> List[Type[ShipType]]:

View File

@@ -17,6 +17,7 @@ from game import db
from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager
from gen import naming
from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
@@ -119,6 +120,7 @@ class Game:
self.enemy_budget = enemy_budget
self.current_unit_id = 0
self.current_group_id = 0
self.name_generator = naming.namegen
self.conditions = self.generate_conditions()
@@ -311,6 +313,14 @@ class Game:
raise RuntimeError(f"{event} was passed when an Event type was expected")
def on_load(self, game_still_initializing: bool = False) -> None:
if not hasattr(self, "name_generator"):
self.name_generator = naming.namegen
# Hack: Replace the global name generator state with the state from the save
# game.
#
# We need to persist this state so that names generated after game load don't
# conflict with those generated before exit.
naming.namegen = self.name_generator
LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater)
self.compute_conflicts_position()
@@ -338,11 +348,14 @@ class Game:
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
self.transfers.perform_transfers()
# Needs to happen *before* planning transfers so we don't cancel the
# Needs to happen *before* planning transfers so we don't cancel them.
self.reset_ato()
for control_point in self.theater.controlpoints:
control_point.process_turn(self)
self.blue_air_wing.replenish()
self.red_air_wing.replenish()
if not skipped and self.turn > 1:
for cp in self.theater.player_points():
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)

View File

@@ -6,6 +6,7 @@ from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
from dcs.unittype import FlyingType
from game.dcs.aircrafttype import AircraftType
from gen.flights.flight import Flight
if TYPE_CHECKING:
@@ -17,9 +18,9 @@ class ControlPointAircraftInventory:
def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point
self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
self.inventory: Dict[AircraftType, int] = defaultdict(int)
def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
def add_aircraft(self, aircraft: AircraftType, count: int) -> None:
"""Adds aircraft to the inventory.
Args:
@@ -28,7 +29,7 @@ class ControlPointAircraftInventory:
"""
self.inventory[aircraft] += count
def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
def remove_aircraft(self, aircraft: AircraftType, count: int) -> None:
"""Removes aircraft from the inventory.
Args:
@@ -42,12 +43,12 @@ class ControlPointAircraftInventory:
available = self.inventory[aircraft]
if available < count:
raise ValueError(
f"Cannot remove {count} {aircraft.id} from "
f"Cannot remove {count} {aircraft} from "
f"{self.control_point.name}. Only have {available}."
)
self.inventory[aircraft] -= count
def available(self, aircraft: Type[FlyingType]) -> int:
def available(self, aircraft: AircraftType) -> int:
"""Returns the number of available aircraft of the given type.
Args:
@@ -59,14 +60,14 @@ class ControlPointAircraftInventory:
return 0
@property
def types_available(self) -> Iterator[Type[FlyingType]]:
def types_available(self) -> Iterator[AircraftType]:
"""Iterates over all available aircraft types."""
for aircraft, count in self.inventory.items():
if count > 0:
yield aircraft
@property
def all_aircraft(self) -> Iterator[Tuple[Type[FlyingType], int]]:
def all_aircraft(self) -> Iterator[Tuple[AircraftType, int]]:
"""Iterates over all available aircraft types, including amounts."""
for aircraft, count in self.inventory.items():
if count > 0:
@@ -107,9 +108,9 @@ class GlobalAircraftInventory:
return self.inventories[control_point]
@property
def available_types_for_player(self) -> Iterator[Type[FlyingType]]:
def available_types_for_player(self) -> Iterator[AircraftType]:
"""Iterates over all aircraft types available to the player."""
seen: Set[Type[FlyingType]] = set()
seen: Set[AircraftType] = set()
for control_point, inventory in self.inventories.items():
if control_point.captured:
for aircraft in inventory.types_available:

View File

@@ -17,7 +17,7 @@ from dcs.triggers import TriggerStart
from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import TheaterGroundObject
from gen import Conflict, FlightType, VisualGenerator, Bullseye
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.aircraft import AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo
@@ -229,23 +229,7 @@ class Operation:
for flight in flights:
if not flight.client_units:
continue
cls.assign_channels_to_flight(flight, air_support)
@staticmethod
def assign_channels_to_flight(flight: FlightData, air_support: AirSupport) -> None:
"""Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
except KeyError:
logging.warning(f"No aircraft data for {airframe.id}")
return
if aircraft_data.channel_allocator is not None:
aircraft_data.channel_allocator.assign_channels_for_flight(
flight, air_support
)
flight.aircraft_type.assign_channels_for_flight(flight, air_support)
@classmethod
def _create_tacan_registry(
@@ -608,8 +592,7 @@ class Operation:
zone = data["zone"]
laserCode = data["laserCode"]
dcsUnit = data["dcsUnit"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
# lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone={repr(zone)}, laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
lua += "}"
# Process the Target Points

View File

@@ -3,12 +3,12 @@ from __future__ import annotations
import math
import random
from dataclasses import dataclass
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple, Type
from dcs.unittype import FlyingType, VehicleType
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
from game import db
from game.data.groundunitclass import GroundUnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.factions.faction import Faction
from game.theater import ControlPoint, MissionTarget
from game.utils import Distance
@@ -59,6 +59,18 @@ class ProcurementAi:
def calculate_ground_unit_budget_share(self) -> float:
armor_investment = 0
aircraft_investment = 0
# faction has no ground units
if (
len(self.faction.artillery_units) == 0
and len(self.faction.frontline_units) == 0
):
return 0
# faction has no planes
if len(self.faction.aircrafts) == 0:
return 1
for cp in self.owned_points:
cp_ground_units = cp.allocated_ground_units(self.game.transfers)
armor_investment += cp_ground_units.total_value
@@ -113,7 +125,7 @@ class ProcurementAi:
if available % 2 == 0:
continue
inventory.remove_aircraft(aircraft, 1)
total += db.PRICES[aircraft]
total += aircraft.price
return total
def repair_runways(self, budget: float) -> float:
@@ -135,12 +147,17 @@ class ProcurementAi:
def affordable_ground_unit_of_class(
self, budget: float, unit_class: GroundUnitClass
) -> Optional[Type[VehicleType]]:
) -> Optional[GroundUnitType]:
faction_units = set(self.faction.frontline_units) | set(
self.faction.artillery_units
)
of_class = set(unit_class.unit_list) & faction_units
affordable_units = [u for u in of_class if db.PRICES[u] <= budget]
of_class = {u for u in faction_units if u.unit_class is unit_class}
# faction has no access to needed unit type, take a random unit
if not of_class:
of_class = faction_units
affordable_units = [u for u in of_class if u.price <= budget]
if not affordable_units:
return None
return random.choice(affordable_units)
@@ -162,7 +179,7 @@ class ProcurementAi:
# Can't afford any more units.
break
budget -= db.PRICES[unit]
budget -= unit.price
cp.pending_unit_deliveries.order({unit: 1})
return budget
@@ -198,12 +215,12 @@ class ProcurementAi:
airbase: ControlPoint,
number: int,
max_price: float,
) -> Optional[Type[FlyingType]]:
best_choice: Optional[Type[FlyingType]] = None
) -> Optional[AircraftType]:
best_choice: Optional[AircraftType] = None
for unit in aircraft_for_task(task):
if unit not in self.faction.aircrafts:
continue
if db.PRICES[unit] * number > max_price:
if unit.price * number > max_price:
continue
if not airbase.can_operate(unit):
continue
@@ -224,7 +241,7 @@ class ProcurementAi:
def affordable_aircraft_for(
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
) -> Optional[Type[FlyingType]]:
) -> Optional[AircraftType]:
return self._affordable_aircraft_for_task(
request.task_capability, airbase, request.number, budget
)
@@ -242,7 +259,7 @@ class ProcurementAi:
# able to operate expensive aircraft.
continue
budget -= db.PRICES[unit] * request.number
budget -= unit.price * request.number
airbase.pending_unit_deliveries.order({unit: request.number})
return budget, True
return budget, False
@@ -343,9 +360,9 @@ class ProcurementAi:
class_cost = 0
total_cost = 0
for unit_type, count in allocations.all.items():
cost = db.PRICES[unit_type] * count
cost = unit_type.price * count
total_cost += cost
if unit_type in unit_class:
if unit_type.unit_class is unit_class:
class_cost += cost
if not total_cost:
return 0

298
game/radio/channels.py Normal file
View File

@@ -0,0 +1,298 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Any, TYPE_CHECKING
if TYPE_CHECKING:
from gen import FlightData, AirSupport
class RadioChannelAllocator:
"""Base class for radio channel allocators."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
"""Assigns mission frequencies to preset channels for the flight."""
raise NotImplementedError
@classmethod
def from_cfg(cls, cfg: dict[str, Any]) -> RadioChannelAllocator:
return cls()
@classmethod
def name(cls) -> str:
raise NotImplementedError
@dataclass(frozen=True)
class CommonRadioChannelAllocator(RadioChannelAllocator):
"""Radio channel allocator suitable for most aircraft.
Most of the aircraft with preset channels available have one or more radios
with 20 or more channels available (typically per-radio, but this is not the
case for the JF-17).
"""
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
inter_flight_radio_index: Optional[int]
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
intra_flight_radio_index: Optional[int]
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
if self.intra_flight_radio_index is not None:
flight.assign_channel(
self.intra_flight_radio_index, 1, flight.intra_flight_channel
)
if self.inter_flight_radio_index is None:
return
# For cases where the inter-flight and intra-flight radios share presets
# (the JF-17 only has one set of channels, even though it can use two
# channels simultaneously), start assigning inter-flight channels at 2.
radio_id = self.inter_flight_radio_index
if self.intra_flight_radio_index == radio_id:
first_channel = 2
else:
first_channel = 1
last_channel = flight.num_radio_channels(radio_id)
channel_alloc = iter(range(first_channel, last_channel + 1))
if flight.departure.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
# TODO: If there ever are multiple AWACS, limit to mission relevant.
for awacs in air_support.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
if flight.arrival != flight.departure and flight.arrival.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
try:
# TODO: Skip incompatible tankers.
for tanker in air_support.tankers:
flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
if flight.divert is not None and flight.divert.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.divert.atc)
except StopIteration:
# Any remaining channels are nice-to-haves, but not necessary for
# the few aircraft with a small number of channels available.
pass
@classmethod
def from_cfg(cls, cfg: dict[str, Any]) -> CommonRadioChannelAllocator:
return CommonRadioChannelAllocator(
inter_flight_radio_index=cfg["inter_flight_radio_index"],
intra_flight_radio_index=cfg["intra_flight_radio_index"],
)
@classmethod
def name(cls) -> str:
return "common"
@dataclass(frozen=True)
class NoOpChannelAllocator(RadioChannelAllocator):
"""Channel allocator for aircraft that don't support preset channels."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
pass
@classmethod
def name(cls) -> str:
return "noop"
@dataclass(frozen=True)
class FarmerRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the MiG-19P."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
# The Farmer only has 6 preset channels. It also only has a VHF radio,
# and currently our ATC data and AWACS are only in the UHF band.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
# TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
# TODO: Assign 2 and 3 to AWACS if it is VHF.
@classmethod
def name(cls) -> str:
return "farmer"
@dataclass(frozen=True)
class ViggenRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the AJS37."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
# The Viggen's preset channels are handled differently from other
# aircraft. The aircraft automatically configures channels for every
# allied flight in the game (including AWACS) and for every airfield. As
# such, we don't need to allocate any of those. There are seven presets
# we can modify, however: three channels for the main radio intended for
# communication with wingmen, and four emergency channels for the backup
# radio. We'll set the first channel of the main radio to the
# intra-flight channel, and the first three emergency channels to each
# of the flight plan's airfields. The fourth emergency channel is always
# the guard channel.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 4, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 5, flight.arrival.atc)
# TODO: Assign divert to 6 when we support divert airfields.
@classmethod
def name(cls) -> str:
return "viggen"
@dataclass(frozen=True)
class SCR522RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 2, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
@classmethod
def name(cls) -> str:
return "SCR-522"
class ChannelNamer:
"""Base class allowing channel name customization per-aircraft.
Most aircraft will want to customize this behavior, but the default is
reasonable for any aircraft with numbered radios.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
"""Returns the name of the channel for the given radio and channel."""
return f"COMM{radio_id} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "default"
class SingleRadioChannelNamer(ChannelNamer):
"""Channel namer for the aircraft with only a single radio.
Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
it's not necessary for us to name the radio when naming the channel.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "single"
class HueyChannelNamer(ChannelNamer):
"""Channel namer for the UH-1H."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM3 Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "huey"
class MirageChannelNamer(ChannelNamer):
"""Channel namer for the M-2000."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["V/UHF", "UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "mirage"
class TomcatChannelNamer(ChannelNamer):
"""Channel namer for the F-14."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "tomcat"
class ViggenChannelNamer(ChannelNamer):
"""Channel namer for the AJS37."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id >= 4:
channel_letter = "EFGH"[channel_id - 4]
return f"FR 24 {channel_letter}"
return f"FR 22 Special {channel_id}"
@classmethod
def name(cls) -> str:
return "viggen"
class ViperChannelNamer(ChannelNamer):
"""Channel namer for the F-16."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM{radio_id} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "viper"
class SCR522ChannelNamer(ChannelNamer):
"""
Channel namer for P-51 & P-47D
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id > 3:
return "?"
else:
return f"Button " + "ABCD"[channel_id - 1]
@classmethod
def name(cls) -> str:
return "SCR-522"

View File

@@ -20,6 +20,7 @@ class Settings:
# Difficulty settings
player_skill: str = "Good"
enemy_skill: str = "Average"
ai_pilot_levelling: bool = True
enemy_vehicle_skill: str = "Average"
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
labels: str = "Full"
@@ -33,6 +34,15 @@ class Settings:
player_income_multiplier: float = 1.0
enemy_income_multiplier: float = 1.0
#: The maximum number of pilots a squadron can have at one time. Changing this after
#: the campaign has started will have no immediate effect; pilots already in the
#: squadron will not be removed if the limit is lowered and pilots will not be
#: immediately created if the limit is raised.
squadron_pilot_limit: int = 24
#: The number of pilots a squadron can replace per turn.
squadron_replenishment_rate: int = 4
default_start_type: str = "Cold"
# Mission specific

View File

@@ -8,7 +8,6 @@ from dataclasses import dataclass, field
from enum import unique, Enum
from pathlib import Path
from typing import (
Type,
Tuple,
TYPE_CHECKING,
Optional,
@@ -17,10 +16,9 @@ from typing import (
)
import yaml
from dcs.unittype import FlyingType
from faker import Faker
from game.db import flying_type_from_name
from game.dcs.aircrafttype import AircraftType
from game.settings import AutoAtoBehavior
if TYPE_CHECKING:
@@ -76,14 +74,23 @@ class Pilot:
@dataclass
class Squadron:
name: str
nickname: str
nickname: Optional[str]
country: str
role: str
aircraft: Type[FlyingType]
aircraft: AircraftType
livery: Optional[str]
mission_types: tuple[FlightType, ...]
pilots: list[Pilot]
available_pilots: list[Pilot] = field(init=False, hash=False, compare=False)
#: The pool of pilots that have not yet been assigned to the squadron. This only
#: happens when a preset squadron defines more preset pilots than the squadron limit
#: allows. This pool will be consumed before random pilots are generated.
pilot_pool: list[Pilot]
current_roster: list[Pilot] = field(default_factory=list, init=False, hash=False)
available_pilots: list[Pilot] = field(
default_factory=list, init=False, hash=False, compare=False
)
auto_assignable_mission_types: set[FlightType] = field(
init=False, hash=False, compare=False
)
@@ -95,18 +102,19 @@ class Squadron:
player: bool
def __post_init__(self) -> None:
self.available_pilots = list(self.active_pilots)
if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
raise ValueError("Squadrons can only be created with active pilots.")
self._recruit_pilots(self.game.settings.squadron_pilot_limit)
self.auto_assignable_mission_types = set(self.mission_types)
def __str__(self) -> str:
if self.nickname is None:
return self.name
return f'{self.name} "{self.nickname}"'
def claim_available_pilot(self) -> Optional[Pilot]:
# No pilots available, so the preference is irrelevant. Create a new pilot and
# return it.
if not self.available_pilots:
self.enlist_new_pilots(1)
return self.available_pilots.pop()
return None
# For opfor, so player/AI option is irrelevant.
if not self.player:
@@ -127,11 +135,12 @@ class Squadron:
# No pilot was found that matched the user's preference.
#
# If they chose to *never* assign players and only players remain in the pool,
# we cannot fill the slot with the available pilots. Recruit a new one.
# we cannot fill the slot with the available pilots.
#
# If they prefer players and we're out of players, just return an AI pilot.
# If they only *prefer* players and we're out of players, just return an AI
# pilot.
if not prefer_players:
self.enlist_new_pilots(1)
return None
return self.available_pilots.pop()
def claim_pilot(self, pilot: Pilot) -> None:
@@ -151,23 +160,45 @@ class Squadron:
# repopulating the same size flight from the same squadron.
self.available_pilots.extend(reversed(pilots))
def enlist_new_pilots(self, count: int) -> None:
new_pilots = [Pilot(self.faker.name()) for _ in range(count)]
self.pilots.extend(new_pilots)
def _recruit_pilots(self, count: int) -> None:
new_pilots = self.pilot_pool[:count]
self.pilot_pool = self.pilot_pool[count:]
count -= len(new_pilots)
new_pilots.extend([Pilot(self.faker.name()) for _ in range(count)])
self.current_roster.extend(new_pilots)
self.available_pilots.extend(new_pilots)
def replenish_lost_pilots(self) -> None:
replenish_count = min(
self.game.settings.squadron_replenishment_rate,
self.number_of_unfilled_pilot_slots,
)
if replenish_count > 0:
self._recruit_pilots(replenish_count)
def return_all_pilots(self) -> None:
self.available_pilots = list(self.active_pilots)
@staticmethod
def send_on_leave(pilot: Pilot) -> None:
pilot.send_on_leave()
def return_from_leave(self, pilot: Pilot):
if not self.has_unfilled_pilot_slots:
raise RuntimeError(
f"Cannot return {pilot} from leave because {self} is full"
)
pilot.return_from_leave()
@property
def faker(self) -> Faker:
return self.game.faker_for(self.player)
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.pilots if p.status == status]
return [p for p in self.current_roster if p.status == status]
def _pilots_without_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.pilots if p.status != status]
return [p for p in self.current_roster if p.status != status]
@property
def active_pilots(self) -> list[Pilot]:
@@ -178,27 +209,44 @@ class Squadron:
return self._pilots_with_status(PilotStatus.OnLeave)
@property
def number_of_pilots_including_dead(self) -> int:
return len(self.pilots)
def number_of_pilots_including_inactive(self) -> int:
return len(self.current_roster)
@property
def number_of_living_pilots(self) -> int:
return len(self._pilots_without_status(PilotStatus.Dead))
def number_of_unfilled_pilot_slots(self) -> int:
return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
@property
def number_of_available_pilots(self) -> int:
return len(self.available_pilots)
@property
def has_available_pilots(self) -> bool:
return bool(self.available_pilots)
@property
def has_unfilled_pilot_slots(self) -> bool:
return self.number_of_unfilled_pilot_slots > 0
def can_auto_assign(self, task: FlightType) -> bool:
return task in self.auto_assignable_mission_types
def pilot_at_index(self, index: int) -> Pilot:
return self.pilots[index]
return self.current_roster[index]
@classmethod
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
from gen.flights.flight import FlightType
with path.open() as squadron_file:
with path.open(encoding="utf8") as squadron_file:
data = yaml.safe_load(squadron_file)
unit_type = flying_type_from_name(data["aircraft"])
if unit_type is None:
raise KeyError(f"Could not find any aircraft with the ID {unit_type}")
name = data["aircraft"]
try:
unit_type = AircraftType.named(name)
except KeyError as ex:
raise KeyError(f"Could not find any aircraft named {name}") from ex
pilots = [Pilot(n, player=False) for n in data.get("pilots", [])]
pilots.extend([Pilot(n, player=True) for n in data.get("players", [])])
@@ -215,13 +263,13 @@ class Squadron:
return Squadron(
name=data["name"],
nickname=data["nickname"],
nickname=data.get("nickname"),
country=data["country"],
role=data["role"],
aircraft=unit_type,
livery=data.get("livery"),
mission_types=tuple(mission_types),
pilots=pilots,
pilot_pool=pilots,
game=game,
player=player,
)
@@ -245,8 +293,8 @@ class SquadronLoader:
yield Path(persistency.base_path()) / "Liberation/Squadrons"
yield Path("resources/squadrons")
def load(self) -> dict[Type[FlyingType], list[Squadron]]:
squadrons: dict[Type[FlyingType], list[Squadron]] = defaultdict(list)
def load(self) -> dict[AircraftType, list[Squadron]]:
squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
country = self.game.country_for(self.player)
faction = self.game.faction_for(self.player)
any_country = country.startswith("Combined Joint Task Forces ")
@@ -311,13 +359,13 @@ class AirWing:
aircraft=aircraft,
livery=None,
mission_types=tuple(tasks_for_aircraft(aircraft)),
pilots=[],
pilot_pool=[],
game=game,
player=player,
)
]
def squadrons_for(self, aircraft: Type[FlyingType]) -> Sequence[Squadron]:
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
return self.squadrons[aircraft]
def squadrons_for_task(self, task: FlightType) -> Iterator[Squadron]:
@@ -325,7 +373,14 @@ class AirWing:
if task in squadron.mission_types:
yield squadron
def squadron_for(self, aircraft: Type[FlyingType]) -> Squadron:
def auto_assignable_for_task_with_type(
self, aircraft: AircraftType, task: FlightType
) -> Iterator[Squadron]:
for squadron in self.squadrons_for(aircraft):
if squadron.can_auto_assign(task) and squadron.has_available_pilots:
yield squadron
def squadron_for(self, aircraft: AircraftType) -> Squadron:
return self.squadrons_for(aircraft)[0]
def iter_squadrons(self) -> Iterator[Squadron]:
@@ -334,6 +389,10 @@ class AirWing:
def squadron_at_index(self, index: int) -> Squadron:
return list(self.iter_squadrons())[index]
def replenish(self) -> None:
for squadron in self.iter_squadrons():
squadron.replenish_lost_pilots()
def reset(self) -> None:
for squadron in self.iter_squadrons():
squadron.return_all_pilots()

View File

@@ -1,11 +1,10 @@
import itertools
import logging
import typing
from typing import Dict, Type
from typing import Any
from dcs.unittype import FlyingType, VehicleType, UnitType
from game.db import PRICES
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.unittype import UnitType
BASE_MAX_STRENGTH = 1
BASE_MIN_STRENGTH = 0
@@ -13,8 +12,8 @@ BASE_MIN_STRENGTH = 0
class Base:
def __init__(self):
self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: Dict[Type[VehicleType], int] = {}
self.aircraft: dict[AircraftType, int] = {}
self.armor: dict[GroundUnitType, int] = {}
self.strength = 1
@property
@@ -29,13 +28,10 @@ class Base:
def total_armor_value(self) -> int:
total = 0
for unit_type, count in self.armor.items():
try:
total += PRICES[unit_type] * count
except KeyError:
logging.exception(f"No price found for {unit_type.id}")
total += unit_type.price * count
return total
def total_units_of_type(self, unit_type) -> int:
def total_units_of_type(self, unit_type: UnitType) -> int:
return sum(
[
c
@@ -44,43 +40,40 @@ class Base:
]
)
def commission_units(self, units: typing.Dict[typing.Type[UnitType], int]):
def commission_units(self, units: dict[Any, int]):
for unit_type, unit_count in units.items():
if unit_count <= 0:
continue
if issubclass(unit_type, VehicleType):
target_dict = self.armor
elif issubclass(unit_type, FlyingType):
target_dict: dict[Any, int]
if isinstance(unit_type, AircraftType):
target_dict = self.aircraft
elif isinstance(unit_type, GroundUnitType):
target_dict = self.armor
else:
logging.error(
f"Unexpected unit type of {unit_type}: "
f"{unit_type.__module__}.{unit_type.__name__}"
)
logging.error(f"Unexpected unit type of {unit_type}")
return
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]):
def commit_losses(self, units_lost: dict[Any, int]):
for unit_type, count in units_lost.items():
target_dict: dict[Any, int]
if unit_type in self.aircraft:
target_array = self.aircraft
target_dict = self.aircraft
elif unit_type in self.armor:
target_array = self.armor
target_dict = self.armor
else:
print("Base didn't find event type {}".format(unit_type))
continue
if unit_type not in target_array:
if unit_type not in target_dict:
print("Base didn't find event type {}".format(unit_type))
continue
target_array[unit_type] = max(target_array[unit_type] - count, 0)
if target_array[unit_type] == 0:
del target_array[unit_type]
target_dict[unit_type] = max(target_dict[unit_type] - count, 0)
if target_dict[unit_type] == 0:
del target_dict[unit_type]
def affect_strength(self, amount):
self.strength += amount

View File

@@ -16,10 +16,10 @@ from dcs.country import Country
from dcs.mapping import Point
from dcs.planes import F_15C
from dcs.ships import (
Bulker_Handy_Wind,
CVN_74_John_C__Stennis,
DDG_Arleigh_Burke_IIa,
LHA_1_Tarawa,
HandyWind,
Stennis,
USS_Arleigh_Burke_IIa,
LHA_Tarawa,
)
from dcs.statics import Fortification, Warehouse
from dcs.terrain import (
@@ -78,53 +78,53 @@ class MizCampaignLoader:
OFF_MAP_UNIT_TYPE = F_15C.id
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
LHA_UNIT_TYPE = LHA_1_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
SHIPPING_LANE_UNIT_TYPE = Bulker_Handy_Wind.id
CV_UNIT_TYPE = Stennis.id
LHA_UNIT_TYPE = LHA_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.M_113.id
SHIPPING_LANE_UNIT_TYPE = HandyWind.id
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
FOB_UNIT_TYPE = Unarmed.SKP_11.id
FARP_HELIPAD = "SINGLE_HELIPAD"
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id
MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id
# Multiple options for air defenses so campaign designers can more accurately see
# the coverage of their IADS for the expected type.
LONG_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Patriot_LN.id,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id,
AirDefence.Patriot_ln.id,
AirDefence.S_300PS_5P85C_ln.id,
AirDefence.S_300PS_5P85D_ln.id,
}
MEDIUM_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Hawk_LN_M192.id,
AirDefence.SAM_SA_2_S_75_Guideline_LN.id,
AirDefence.SAM_SA_3_S_125_Goa_LN.id,
AirDefence.Hawk_ln.id,
AirDefence.S_75M_Volhov.id,
AirDefence._5p73_s_125_ln.id,
}
SHORT_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Avenger__Stinger.id,
AirDefence.SAM_Rapier_LN.id,
AirDefence.SAM_SA_19_Tunguska_Grison.id,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id,
AirDefence.M1097_Avenger.id,
AirDefence.Rapier_fsa_launcher.id,
AirDefence._2S6_Tunguska.id,
AirDefence.Strela_1_9P31.id,
}
AAA_UNIT_TYPES = {
AirDefence.AAA_8_8cm_Flak_18.id,
AirDefence.SPAAA_Vulcan_M163.id,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id,
AirDefence.Flak18.id,
AirDefence.Vulcan.id,
AirDefence.ZSU_23_4_Shilka.id,
}
EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
EWR_UNIT_TYPE = AirDefence._1L13_EWR.id
ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id
ARMOR_GROUP_UNIT_TYPE = Armor.M_1_Abrams.id
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse._Ammunition_depot.id
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id

View File

@@ -16,7 +16,6 @@ from typing import (
Optional,
Set,
TYPE_CHECKING,
Type,
Union,
Sequence,
Iterable,
@@ -25,14 +24,13 @@ from typing import (
from dcs.mapping import Point
from dcs.ships import (
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
LHA_1_Tarawa,
Type_071_Amphibious_Transport_Dock,
Stennis,
KUZNECOW,
LHA_Tarawa,
Type_071,
)
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit
from dcs.unittype import FlyingType, VehicleType
from game import db
from game.point_with_heading import PointWithHeading
@@ -48,6 +46,8 @@ from .theatergroundobject import (
)
from ..db import PRICES
from ..helipad import Helipad
from ..dcs.aircrafttype import AircraftType
from ..dcs.groundunittype import GroundUnitType
from ..utils import nautical_miles
from ..weather import Conditions
@@ -126,19 +126,19 @@ class PresetLocations:
@dataclass(frozen=True)
class AircraftAllocations:
present: dict[Type[FlyingType], int]
ordered: dict[Type[FlyingType], int]
transferring: dict[Type[FlyingType], int]
present: dict[AircraftType, int]
ordered: dict[AircraftType, int]
transferring: dict[AircraftType, int]
@property
def total_value(self) -> int:
total: int = 0
for unit_type, count in self.present.items():
total += PRICES[unit_type] * count
total += unit_type.price * count
for unit_type, count in self.ordered.items():
total += PRICES[unit_type] * count
total += unit_type.price * count
for unit_type, count in self.transferring.items():
total += PRICES[unit_type] * count
total += unit_type.price * count
return total
@@ -161,13 +161,13 @@ class AircraftAllocations:
@dataclass(frozen=True)
class GroundUnitAllocations:
present: dict[Type[VehicleType], int]
ordered: dict[Type[VehicleType], int]
transferring: dict[Type[VehicleType], int]
present: dict[GroundUnitType, int]
ordered: dict[GroundUnitType, int]
transferring: dict[GroundUnitType, int]
@property
def all(self) -> dict[Type[VehicleType], int]:
combined: dict[Type[VehicleType], int] = defaultdict(int)
def all(self) -> dict[GroundUnitType, int]:
combined: dict[GroundUnitType, int] = defaultdict(int)
for unit_type, count in itertools.chain(
self.present.items(), self.ordered.items(), self.transferring.items()
):
@@ -178,11 +178,11 @@ class GroundUnitAllocations:
def total_value(self) -> int:
total: int = 0
for unit_type, count in self.present.items():
total += PRICES[unit_type] * count
total += unit_type.price * count
for unit_type, count in self.ordered.items():
total += PRICES[unit_type] * count
total += unit_type.price * count
for unit_type, count in self.transferring.items():
total += PRICES[unit_type] * count
total += unit_type.price * count
return total
@@ -510,14 +510,14 @@ class ControlPoint(MissionTarget, ABC):
for group in g.groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
Stennis,
KUZNECOW,
]:
return group.name
elif g.dcs_identifier == "LHA":
for group in g.groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]:
if db.unit_type_from_name(u.type) in [LHA_Tarawa]:
return group.name
return None
@@ -568,24 +568,16 @@ class ControlPoint(MissionTarget, ABC):
destination.control_point.base.commission_units({unit_type: 1})
destination = heapq.heappushpop(destinations, destination)
def capture_aircraft(
self, game: Game, airframe: Type[FlyingType], count: int
) -> None:
try:
value = PRICES[airframe] * count
except KeyError:
logging.exception(f"Unknown price for {airframe.id}")
return
def capture_aircraft(self, game: Game, airframe: AircraftType, count: int) -> None:
value = airframe.price * count
game.adjust_budget(value, player=not self.captured)
game.message(
f"No valid retreat destination in range of {self.name} for "
f"{airframe.id}. {count} aircraft have been captured and sold for "
f"${value}M."
f"No valid retreat destination in range of {self.name} for {airframe}"
f"{count} aircraft have been captured and sold for ${value}M."
)
def aircraft_retreat_destination(
self, game: Game, airframe: Type[FlyingType]
self, game: Game, airframe: AircraftType
) -> Optional[ControlPoint]:
closest = ObjectiveDistanceCache.get_closest_airfields(self)
# TODO: Should be airframe dependent.
@@ -603,10 +595,10 @@ class ControlPoint(MissionTarget, ABC):
return None
def _retreat_air_units(
self, game: Game, airframe: Type[FlyingType], count: int
self, game: Game, airframe: AircraftType, count: int
) -> None:
while count:
logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
logging.debug(f"Retreating {count} {airframe} from {self.name}")
destination = self.aircraft_retreat_destination(game, airframe)
if destination is None:
self.capture_aircraft(game, airframe, count)
@@ -642,16 +634,16 @@ class ControlPoint(MissionTarget, ABC):
self.base.set_strength_to_minimum()
@abstractmethod
def can_operate(self, aircraft: Type[FlyingType]) -> bool:
def can_operate(self, aircraft: AircraftType) -> bool:
...
def aircraft_transferring(self, game: Game) -> dict[Type[FlyingType], int]:
def aircraft_transferring(self, game: Game) -> dict[AircraftType, int]:
if self.captured:
ato = game.blue_ato
else:
ato = game.red_ato
transferring: defaultdict[Type[FlyingType], int] = defaultdict(int)
transferring: defaultdict[AircraftType, int] = defaultdict(int)
for package in ato.packages:
for flight in package.flights:
if flight.departure == flight.arrival:
@@ -716,7 +708,7 @@ class ControlPoint(MissionTarget, ABC):
def allocated_aircraft(self, game: Game) -> AircraftAllocations:
on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items():
if issubclass(unit_bought, FlyingType):
if isinstance(unit_bought, AircraftType):
on_order[unit_bought] = count
return AircraftAllocations(
@@ -728,10 +720,10 @@ class ControlPoint(MissionTarget, ABC):
) -> GroundUnitAllocations:
on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items():
if issubclass(unit_bought, VehicleType):
if isinstance(unit_bought, GroundUnitType):
on_order[unit_bought] = count
transferring: dict[Type[VehicleType], int] = defaultdict(int)
transferring: dict[GroundUnitType, int] = defaultdict(int)
for transfer in transfers:
if transfer.destination == self:
for unit_type, count in transfer.units.items():
@@ -828,7 +820,7 @@ class Airfield(ControlPoint):
self.airport = airport
self._runway_status = RunwayStatus()
def can_operate(self, aircraft: FlyingType) -> bool:
def can_operate(self, aircraft: AircraftType) -> bool:
# TODO: Allow helicopters.
# Need to implement ground spawns so the helos don't use the runway.
# TODO: Allow harrier.
@@ -940,10 +932,10 @@ class NavalControlPoint(ControlPoint, ABC):
for group in self.find_main_tgo().groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
CV_1143_5_Admiral_Kuznetsov,
Type_071_Amphibious_Transport_Dock,
Stennis,
LHA_Tarawa,
KUZNECOW,
Type_071,
]:
return True
return False
@@ -1012,8 +1004,8 @@ class Carrier(NavalControlPoint):
def is_carrier(self):
return True
def can_operate(self, aircraft: FlyingType) -> bool:
return aircraft in db.CARRIER_CAPABLE
def can_operate(self, aircraft: AircraftType) -> bool:
return aircraft.carrier_capable
@property
def total_aircraft_parking(self) -> int:
@@ -1046,8 +1038,8 @@ class Lha(NavalControlPoint):
def is_lha(self) -> bool:
return True
def can_operate(self, aircraft: FlyingType) -> bool:
return aircraft in db.LHA_CAPABLE
def can_operate(self, aircraft: AircraftType) -> bool:
return aircraft.lha_capable
@property
def total_aircraft_parking(self) -> int:
@@ -1086,7 +1078,7 @@ class OffMapSpawn(ControlPoint):
def total_aircraft_parking(self) -> int:
return 1000
def can_operate(self, aircraft: FlyingType) -> bool:
def can_operate(self, aircraft: AircraftType) -> bool:
return True
@property

View File

@@ -597,7 +597,8 @@ class EwrGroundObject(TheaterGroundObject):
@property
def group_name(self) -> str:
# Prefix the group names with the side color so Skynet can find them.
return f"{self.faction_color}|{super().group_name}"
# Use Group Id and uppercase EWR
return f"{self.faction_color}|EWR|{self.group_id}"
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType

View File

@@ -12,14 +12,14 @@ from typing import (
List,
Optional,
TYPE_CHECKING,
Type,
TypeVar,
Sequence,
)
from dcs.mapping import Point
from dcs.unittype import FlyingType, VehicleType
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.procurement import AircraftProcurementRequest
from game.squadrons import Squadron
from game.theater import ControlPoint, MissionTarget
@@ -29,7 +29,7 @@ from game.theater.transitnetwork import (
)
from game.utils import meters, nautical_miles
from gen.ato import Package
from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE
from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE, aircraft_for_task
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight, FlightType
from gen.flights.flightplan import FlightPlanBuilder
@@ -72,7 +72,7 @@ class TransferOrder:
player: bool = field(init=False)
#: The units being transferred.
units: Dict[Type[VehicleType], int]
units: Dict[GroundUnitType, int]
transport: Optional[Transport] = field(default=None)
@@ -89,7 +89,7 @@ class TransferOrder:
def kill_all(self) -> None:
self.units.clear()
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
def kill_unit(self, unit_type: GroundUnitType) -> None:
if unit_type not in self.units or not self.units[unit_type]:
raise KeyError(f"{self.destination} has no {unit_type} remaining")
self.units[unit_type] -= 1
@@ -98,7 +98,7 @@ class TransferOrder:
def size(self) -> int:
return sum(c for c in self.units.values())
def iter_units(self) -> Iterator[Type[VehicleType]]:
def iter_units(self) -> Iterator[GroundUnitType]:
for unit_type, count in self.units.items():
for _ in range(count):
yield unit_type
@@ -156,7 +156,7 @@ class Airlift(Transport):
self.flight = flight
@property
def units(self) -> Dict[Type[VehicleType], int]:
def units(self) -> Dict[GroundUnitType, int]:
return self.transfer.units
@property
@@ -191,9 +191,9 @@ class AirliftPlanner:
self.package = Package(target=next_stop, auto_asap=True)
def compatible_with_mission(
self, unit_type: Type[FlyingType], airfield: ControlPoint
self, unit_type: AircraftType, airfield: ControlPoint
) -> bool:
if not unit_type in TRANSPORT_CAPABLE:
if unit_type not in aircraft_for_task(FlightType.TRANSPORT):
return False
if not self.transfer.origin.can_operate(unit_type):
return False
@@ -201,7 +201,7 @@ class AirliftPlanner:
return False
# Cargo planes have no maximum range.
if not unit_type.helicopter:
if not unit_type.dcs_unit_type.helicopter:
return True
# A helicopter that is transport capable and able to operate at both bases. Need
@@ -227,36 +227,42 @@ class AirliftPlanner:
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
self.transfer.position
)
air_wing = self.game.air_wing_for(self.for_player)
for cp in distance_cache.closest_airfields:
if cp.captured != self.for_player:
continue
inventory = self.game.aircraft_inventory.for_control_point(cp)
for unit_type, available in inventory.all_aircraft:
squadrons = [
s
for s in self.game.air_wing_for(self.for_player).squadrons_for(
unit_type
)
if FlightType.TRANSPORT in s.auto_assignable_mission_types
]
if not squadrons:
continue
squadron = squadrons[0]
if self.compatible_with_mission(unit_type, cp):
while available and self.transfer.transport is None:
flight_size = self.create_airlift_flight(squadron, inventory)
available -= flight_size
squadrons = air_wing.auto_assignable_for_task_with_type(
unit_type, FlightType.TRANSPORT
)
for squadron in squadrons:
if self.compatible_with_mission(unit_type, cp):
while (
available
and squadron.has_available_pilots
and self.transfer.transport is None
):
flight_size = self.create_airlift_flight(
squadron, inventory
)
available -= flight_size
if self.package.flights:
self.game.ato_for(self.for_player).add_package(self.package)
def create_airlift_flight(
self, squadron: Squadron, inventory: ControlPointAircraftInventory
) -> int:
available = inventory.available(squadron.aircraft)
capacity_each = 1 if squadron.aircraft.helicopter else 2
available_aircraft = inventory.available(squadron.aircraft)
capacity_each = 1 if squadron.aircraft.dcs_unit_type.helicopter else 2
required = math.ceil(self.transfer.size / capacity_each)
flight_size = min(required, available, squadron.aircraft.group_size_max)
flight_size = min(
required,
available_aircraft,
squadron.aircraft.dcs_unit_type.group_size_max,
squadron.number_of_available_pilots,
)
capacity = flight_size * capacity_each
if capacity < self.transfer.size:
@@ -308,7 +314,7 @@ class MultiGroupTransport(MissionTarget, Transport):
transfer.transport = None
self.transfers.remove(transfer)
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
def kill_unit(self, unit_type: GroundUnitType) -> None:
for transfer in self.transfers:
try:
transfer.kill_unit(unit_type)
@@ -331,13 +337,18 @@ class MultiGroupTransport(MissionTarget, Transport):
return sum(sum(t.units.values()) for t in self.transfers)
@property
def units(self) -> Dict[Type[VehicleType], int]:
units: Dict[Type[VehicleType], int] = defaultdict(int)
def units(self) -> dict[GroundUnitType, int]:
units: Dict[GroundUnitType, int] = defaultdict(int)
for transfer in self.transfers:
for unit_type, count in transfer.units.items():
units[unit_type] += count
return units
def iter_units(self) -> Iterator[GroundUnitType]:
for unit_type, count in self.units.items():
for _ in range(count):
yield unit_type
@property
def player_owned(self) -> bool:
return self.origin.captured

View File

@@ -3,12 +3,11 @@ from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, Optional, TYPE_CHECKING, Type
from dcs.unittype import UnitType, VehicleType
from typing import Dict, Optional, TYPE_CHECKING, Any
from game.theater import ControlPoint
from .db import PRICES
from .dcs.groundunittype import GroundUnitType
from .dcs.unittype import UnitType
from .theater.transitnetwork import (
NoPathError,
TransitNetwork,
@@ -29,16 +28,16 @@ class PendingUnitDeliveries:
self.destination = destination
# Maps unit type to order quantity.
self.units: Dict[Type[UnitType], int] = defaultdict(int)
self.units: Dict[UnitType, int] = defaultdict(int)
def __str__(self) -> str:
return f"Pending delivery to {self.destination}"
def order(self, units: Dict[Type[UnitType], int]) -> None:
def order(self, units: Dict[UnitType, int]) -> None:
for k, v in units.items():
self.units[k] += v
def sell(self, units: Dict[Type[UnitType], int]) -> None:
def sell(self, units: Dict[UnitType, int]) -> None:
for k, v in units.items():
self.units[k] -= v
@@ -46,24 +45,20 @@ class PendingUnitDeliveries:
self.refund(game, self.units)
self.units = defaultdict(int)
def refund(self, game: Game, units: Dict[Type[UnitType], int]) -> None:
def refund(self, game: Game, units: Dict[UnitType, int]) -> None:
for unit_type, count in units.items():
try:
price = PRICES[unit_type]
except KeyError:
logging.error(f"Could not refund {unit_type.id}, price unknown")
continue
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
game.adjust_budget(
unit_type.price * count, player=self.destination.captured
)
logging.info(f"Refunding {count} {unit_type.id} at {self.destination.name}")
game.adjust_budget(price * count, player=self.destination.captured)
def pending_orders(self, unit_type: Type[UnitType]) -> int:
def pending_orders(self, unit_type: UnitType) -> int:
pending_units = self.units.get(unit_type)
if pending_units is None:
pending_units = 0
return pending_units
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
def available_next_turn(self, unit_type: UnitType) -> int:
current_units = self.destination.base.total_units_of_type(unit_type)
return self.pending_orders(unit_type) + current_units
@@ -77,15 +72,14 @@ class PendingUnitDeliveries:
self.refund_all(game)
return
bought_units: Dict[Type[UnitType], int] = {}
units_needing_transfer: Dict[Type[VehicleType], int] = {}
sold_units: Dict[Type[UnitType], int] = {}
bought_units: Dict[UnitType, int] = {}
units_needing_transfer: Dict[GroundUnitType, int] = {}
sold_units: Dict[UnitType, int] = {}
for unit_type, count in self.units.items():
coalition = "Ally" if self.destination.captured else "Enemy"
name = unit_type.id
d: dict[Any, int]
if (
issubclass(unit_type, VehicleType)
isinstance(unit_type, GroundUnitType)
and self.destination != ground_unit_source
):
source = ground_unit_source
@@ -97,11 +91,11 @@ class PendingUnitDeliveries:
if count >= 0:
d[unit_type] = count
game.message(
f"{coalition} reinforcements: {name} x {count} at {source}"
f"{coalition} reinforcements: {unit_type} x {count} at {source}"
)
else:
sold_units[unit_type] = -count
game.message(f"{coalition} sold: {name} x {-count} at {source}")
game.message(f"{coalition} sold: {unit_type} x {-count} at {source}")
self.units = defaultdict(int)
self.destination.base.commission_units(bought_units)
@@ -112,7 +106,7 @@ class PendingUnitDeliveries:
self.create_transfer(game, ground_unit_source, units_needing_transfer)
def create_transfer(
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
self, game: Game, source: ControlPoint, units: Dict[GroundUnitType, int]
) -> None:
game.transfers.new_transfer(TransferOrder(source, self.destination, units))

View File

@@ -2,13 +2,12 @@
import itertools
import math
from dataclasses import dataclass
from typing import Dict, Optional, Type
from typing import Dict, Optional
from dcs.unit import Unit
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
from dcs.unittype import VehicleType
from game import db
from game.dcs.groundunittype import GroundUnitType
from game.squadrons import Pilot
from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
@@ -19,12 +18,12 @@ from gen.flights.flight import Flight
@dataclass(frozen=True)
class FlyingUnit:
flight: Flight
pilot: Pilot
pilot: Optional[Pilot]
@dataclass(frozen=True)
class FrontLineUnit:
unit_type: Type[VehicleType]
unit_type: GroundUnitType
origin: ControlPoint
@@ -37,13 +36,13 @@ class GroundObjectUnit:
@dataclass(frozen=True)
class ConvoyUnit:
unit_type: Type[VehicleType]
unit_type: GroundUnitType
convoy: Convoy
@dataclass(frozen=True)
class AirliftUnits:
cargo: tuple[Type[VehicleType], ...]
cargo: tuple[GroundUnitType, ...]
transfer: TransferOrder
@@ -70,8 +69,6 @@ class UnitMap:
name = str(unit.name)
if name in self.aircraft:
raise RuntimeError(f"Duplicate unit name: {name}")
if pilot is None:
raise ValueError(f"{name} has no pilot assigned")
self.aircraft[name] = FlyingUnit(flight, pilot)
if flight.cargo is not None:
self.add_airlift_units(group, flight.cargo)
@@ -87,20 +84,15 @@ class UnitMap:
def airfield(self, name: str) -> Optional[Airfield]:
return self.airfields.get(name, None)
def add_front_line_units(self, group: Group, origin: ControlPoint) -> None:
def add_front_line_units(
self, group: Group, origin: ControlPoint, unit_type: GroundUnitType
) -> None:
for unit in group.units:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.front_line_units:
raise RuntimeError(f"Duplicate front line unit: {name}")
unit_type = db.unit_type_from_name(unit.type)
if unit_type is None:
raise RuntimeError(f"Unknown unit type: {unit.type}")
if not issubclass(unit_type, VehicleType):
raise RuntimeError(
f"{name} is a {unit_type.__name__}, expected a VehicleType"
)
self.front_line_units[name] = FrontLineUnit(unit_type, origin)
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
@@ -143,19 +135,12 @@ class UnitMap:
return self.ground_object_units.get(name, None)
def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
for unit in group.units:
for unit, unit_type in zip(group.units, convoy.iter_units()):
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.convoys:
raise RuntimeError(f"Duplicate convoy unit: {name}")
unit_type = db.unit_type_from_name(unit.type)
if unit_type is None:
raise RuntimeError(f"Unknown unit type: {unit.type}")
if not issubclass(unit_type, VehicleType):
raise RuntimeError(
f"{name} is a {unit_type.__name__}, expected a VehicleType"
)
self.convoys[name] = ConvoyUnit(unit_type, convoy)
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:

View File

@@ -58,6 +58,10 @@ class Distance:
def from_nautical_miles(cls, value: float) -> Distance:
return cls(value * NM_TO_METERS)
@classmethod
def inf(cls) -> Distance:
return cls.from_meters(math.inf)
def __add__(self, other: Distance) -> Distance:
return meters(self.meters + other.meters)

View File

@@ -2,7 +2,7 @@ from pathlib import Path
def _build_version_string() -> str:
components = ["4.0"]
components = ["4.0.0"]
build_number_path = Path("resources/buildnumber")
if build_number_path.exists():
with build_number_path.open("r") as build_number_file:
@@ -70,9 +70,9 @@ VERSION = _build_version_string()
#: Version 4.2
#: * Adds support for AAA objectives. Place with any of the following units (either red
#: or blue):
#: * AAA_8_8cm_Flak_18,
#: * SPAAA_Vulcan_M163,
#: * SPAAA_ZSU_23_4_Shilka_Gun_Dish,
#: * Flak18,
#: * Vulcan,
#: * ZSU_23_4_Shilka,
#:
#: Version 5.0
#: * Ammunition Depots objective locations are now predetermined using the "Ammunition
@@ -87,4 +87,7 @@ VERSION = _build_version_string()
#: Version 6.0
#: * Random objective generation no is longer supported. Fixed objective locations were
#: added in 4.1.
CAMPAIGN_FORMAT_VERSION = (6, 0)
#:
#: Version 6.1
#: * Support for new Syrian airfields in DCS 2.7.2.7910.1 (Cyprus update).
CAMPAIGN_FORMAT_VERSION = (6, 1)