Dan Albert 1760532168
Rename UnitType.name what it is: the variant ID.
This property affects safe compat because the ID is what gets preserved
in the save, but it's unfortunately also used as the display name, which
means changing the display name breaks save compat. It also prevents us
from changing display names without breaking faction definitions.

This is the first step in fixing that. The next is adding a separate
display_name property that can be updated without breaking either of
those.
2023-10-07 17:08:33 +02:00

584 lines
22 KiB
Python

from __future__ import annotations
import itertools
import logging
from dataclasses import dataclass, field
from functools import cached_property
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING, Set
import dcs
from dcs.country import Country
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType
from game.armedforces.forcegroup import ForceGroup
from game.data.building_data import (
WW2_ALLIES_BUILDINGS,
DEFAULT_AVAILABLE_BUILDINGS,
WW2_GERMANY_BUILDINGS,
WW2_FREE,
REQUIRED_BUILDINGS,
IADS_BUILDINGS,
)
from game.data.doctrine import (
Doctrine,
MODERN_DOCTRINE,
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
)
from game.data.groups import GroupRole
from game.data.units import UnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.countries import country_with_name
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unittype import UnitType
from pydcs_extensions import inject_F15I, eject_F15I
from pydcs_extensions.f16i_idf.f16i_idf import inject_F16I, eject_F16I
if TYPE_CHECKING:
from game.theater.start_generator import ModSettings
@dataclass
class Faction:
#: List of locales to use when generating random names. If not set, Faker will
#: choose the default locale.
locales: Optional[List[str]]
# The unit type to spawn for cargo shipping.
cargo_ship: ShipUnitType
# Country used by this faction
country: Country
# Nice name of the faction
name: str = field(default="")
# List of faction file authors
authors: str = field(default="")
# A description of the faction
description: str = field(default="")
# Available aircraft
aircraft: Set[AircraftType] = field(default_factory=set)
# Available awacs aircraft
awacs: Set[AircraftType] = field(default_factory=set)
# Available tanker aircraft
tankers: Set[AircraftType] = field(default_factory=set)
# Available frontline units
frontline_units: Set[GroundUnitType] = field(default_factory=set)
# Available artillery units
artillery_units: Set[GroundUnitType] = field(default_factory=set)
# Infantry units used
infantry_units: Set[GroundUnitType] = field(default_factory=set)
# Logistics units used
logistics_units: Set[GroundUnitType] = field(default_factory=set)
# Possible Air Defence units, Like EWRs
air_defense_units: Set[GroundUnitType] = field(default_factory=set)
# A list of all supported sets of units
preset_groups: list[ForceGroup] = field(default_factory=list)
# Possible Missile site generators for this faction
missiles: Set[GroundUnitType] = field(default_factory=set)
# Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict)
# Possible carrier names
carrier_names: Set[str] = field(default_factory=set)
# Possible helicopter carrier names
helicopter_carrier_names: Set[str] = field(default_factory=set)
# Available Naval Units
naval_units: Set[ShipUnitType] = field(default_factory=set)
# Whether this faction has JTAC access
has_jtac: bool = field(default=False)
# Unit to use as JTAC for this faction
jtac_unit: Optional[AircraftType] = field(default=None)
# doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
# List of available building layouts for this faction
building_set: Set[str] = field(default_factory=set)
# List of default livery overrides
liveries_overrides: Dict[AircraftType, List[str]] = field(default_factory=dict)
# List of default livery overrides for ground vehicles
liveries_overrides_ground_forces: Dict[str, 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
#: of the time period or operator. For example, the CJTF "countries" don't
#: appear to have GPS capability, so they need this.
#:
#: Note that this option cannot be set per-side. If either faction needs it,
#: both will use it.
unrestricted_satnav: bool = False
# Store mod settings so mod properties can be injected again on game load,
# in case mods like CJS F/A-18E/F/G or IDF F-16I are selected by the player
mod_settings: Optional[ModSettings] = field(default=None)
def has_access_to_dcs_type(self, unit_type: Type[DcsUnitType]) -> bool:
# Vehicle and Ship Units
if any(unit_type == u.dcs_unit_type for u in self.accessible_units):
return True
# Statics
if issubclass(unit_type, StaticType):
# TODO Improve the statics checking
# We currently do not have any list or similar to check if a faction has
# access to a specific static. There we accept any static here
return True
return False
def has_access_to_unit_class(self, unit_class: UnitClass) -> bool:
return any(unit.unit_class is unit_class for unit in self.accessible_units)
@cached_property
def accessible_units(self) -> list[UnitType[Any]]:
all_units: Iterator[UnitType[Any]] = itertools.chain(
self.ground_units,
self.infantry_units,
self.air_defense_units,
self.naval_units,
self.missiles,
(
unit
for preset_group in self.preset_groups
for unit in preset_group.units
),
)
return list(set(all_units))
@property
def air_defenses(self) -> list[str]:
"""Returns the Air Defense types"""
# This is used for the faction overview in NewGameWizard
air_defenses = [a.variant_id for a in self.air_defense_units]
air_defenses.extend(
[
pg.name
for pg in self.preset_groups
if any(task.role == GroupRole.AIR_DEFENSE for task in pg.tasks)
]
)
return sorted(air_defenses)
@cached_property
def aircrafts(self) -> list[UnitType[Any]]:
# Migrator can't cope with this, so we need to do it here...
self.aircraft = set(self.aircraft)
self.awacs = set(self.awacs)
self.tankers = set(self.tankers)
return list(self.aircraft.union(self.awacs.union(self.tankers)))
@classmethod
def from_dict(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
try:
country = country_with_name(json["country"])
except KeyError as ex:
raise KeyError(
f'Faction\'s country ("{json.get("country")}") is not a valid DCS '
"country ID"
) from ex
faction = Faction(
locales=json.get("locales"),
country=country,
cargo_ship=ShipUnitType.named(json.get("cargo_ship", "Bulker Handy Wind")),
)
faction.name = json.get("name", "")
if not faction.name:
raise AssertionError("Faction has no valid name")
faction.authors = json.get("authors", "")
faction.description = json.get("description", "")
faction.aircraft = {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.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.air_defense_units = {
GroundUnitType.named(n) for n in json.get("air_defense_units", [])
}
faction.missiles = {GroundUnitType.named(n) for n in json.get("missiles", [])}
faction.naval_units = {
ShipUnitType.named(n) for n in json.get("naval_units", [])
}
faction.preset_groups = [
ForceGroup.from_preset_group(g) for g in json.get("preset_groups", [])
]
faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None)
if jtac_name is not None:
faction.jtac_unit = AircraftType.named(jtac_name)
else:
faction.jtac_unit = None
# Load doctrine
doctrine = json.get("doctrine", "modern")
if doctrine == "modern":
faction.doctrine = MODERN_DOCTRINE
elif doctrine == "coldwar":
faction.doctrine = COLDWAR_DOCTRINE
elif doctrine == "ww2":
faction.doctrine = WWII_DOCTRINE
else:
faction.doctrine = MODERN_DOCTRINE
# Load the building set
faction.building_set = set()
building_set = json.get("building_set", "default")
if building_set == "default":
faction.building_set.update(DEFAULT_AVAILABLE_BUILDINGS)
elif building_set == "ww2free":
faction.building_set.update(WW2_FREE)
elif building_set == "ww2ally":
faction.building_set.update(WW2_ALLIES_BUILDINGS)
elif building_set == "ww2germany":
faction.building_set.update(WW2_GERMANY_BUILDINGS)
else:
faction.building_set.update(DEFAULT_AVAILABLE_BUILDINGS)
# Add required buildings for the game logic (e.g. ammo, factory..)
faction.building_set.update(REQUIRED_BUILDINGS)
faction.building_set.update(IADS_BUILDINGS)
# Load liveries override
faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {})
for name, livery in liveries_overrides.items():
aircraft = AircraftType.named(name)
faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
# Load liveries override for ground forces
faction.liveries_overrides_ground_forces = {}
liveries_overrides_ground_forces = json.get(
"liveries_overrides_ground_forces", {}
)
for vehicle_type, livery in liveries_overrides_ground_forces.items():
faction.liveries_overrides_ground_forces[vehicle_type] = [
s.lower() for s in livery
]
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
return faction
@property
def ground_units(self) -> Iterator[GroundUnitType]:
yield from self.artillery_units
yield from self.frontline_units
yield from self.logistics_units
def infantry_with_class(self, unit_class: UnitClass) -> Iterator[GroundUnitType]:
for unit in self.infantry_units:
if unit.unit_class is unit_class:
yield unit
def apply_mod_settings(self, mod_settings: Optional[ModSettings] = None) -> None:
if mod_settings is None:
if self.mod_settings is None:
# No mod settings were provided and none were saved for this faction
# so stop here
return
elif self.mod_settings is not None:
# Saved mod settings were found for this faction,
# so load them for use
mod_settings = self.mod_settings
else:
# Update the mod settings of this faction
# so the settings can be applied again on load, if needed
self.mod_settings = mod_settings
# aircraft
if not mod_settings.a4_skyhawk:
self.remove_aircraft("A-4E-C")
if not mod_settings.hercules:
self.remove_aircraft("Hercules")
if not mod_settings.uh_60l:
self.remove_aircraft("UH-60L")
self.remove_aircraft("KC130J")
if not mod_settings.f4bc_phantom:
self.remove_aircraft("VSN_F4B")
self.remove_aircraft("VSN_F4C")
if not mod_settings.f15d_baz:
self.remove_aircraft("F-15D")
if not mod_settings.f_15_idf:
eject_F15I()
if AircraftType.named("F-15I Ra'am") in self.aircraft:
self.aircraft.remove(AircraftType.named("F-15I Ra'am"))
else:
inject_F15I()
if AircraftType.named("F-15E Strike Eagle (Suite 4+)") in self.aircraft:
self.aircraft.add(AircraftType.named("F-15I Ra'am"))
if not mod_settings.f_16_idf:
self.remove_aircraft("F-16I")
self.remove_aircraft("F-16D_52")
self.remove_aircraft("F-16D_50")
self.remove_aircraft("F-16D_50_NS")
self.remove_aircraft("F-16D_52_NS")
self.remove_aircraft("F-16D_Barak_30")
self.remove_aircraft("F-16D_Barak_40")
eject_F16I()
else:
inject_F16I()
if not mod_settings.f22_raptor:
self.remove_aircraft("F-22A")
if not mod_settings.f84g_thunderjet:
self.remove_aircraft("VSN_F84G")
if not mod_settings.f100_supersabre:
self.remove_aircraft("VSN_F100")
if not mod_settings.f104_starfighter:
self.remove_aircraft("VSN_F104C")
self.remove_aircraft("VSN_F104G")
self.remove_aircraft("VSN_F104S")
self.remove_aircraft("VSN_F104S_AG")
if not mod_settings.f105_thunderchief:
self.remove_aircraft("VSN_F105D")
self.remove_aircraft("VSN_F105G")
if not mod_settings.a6a_intruder:
self.remove_aircraft("VSN_A6A")
if not mod_settings.jas39_gripen:
self.remove_aircraft("JAS39Gripen")
self.remove_aircraft("JAS39Gripen_BVR")
self.remove_aircraft("JAS39Gripen_AG")
if not mod_settings.su30_flanker_h:
self.remove_aircraft("Su-30MKA")
self.remove_aircraft("Su-30MKI")
self.remove_aircraft("Su-30MKM")
self.remove_aircraft("Su-30SM")
if not mod_settings.su57_felon:
self.remove_aircraft("Su-57")
if not mod_settings.ov10a_bronco:
self.remove_aircraft("Bronco-OV-10A")
if not mod_settings.a7e_corsair2:
self.remove_aircraft("A-7E")
# frenchpack
if not mod_settings.frenchpack:
self.remove_vehicle("AMX10RCR")
self.remove_vehicle("SEPAR")
self.remove_vehicle("ERC")
self.remove_vehicle("M120")
self.remove_vehicle("AA20")
self.remove_vehicle("TRM2000")
self.remove_vehicle("TRM2000_Citerne")
self.remove_vehicle("TRM2000_AA20")
self.remove_vehicle("TRMMISTRAL")
self.remove_vehicle("VABH")
self.remove_vehicle("VAB_RADIO")
self.remove_vehicle("VAB_50")
self.remove_vehicle("VIB_VBR")
self.remove_vehicle("VAB_HOT")
self.remove_vehicle("VAB_MORTIER")
self.remove_vehicle("VBL50")
self.remove_vehicle("VBLANF1")
self.remove_vehicle("VBL-radio")
self.remove_vehicle("VBAE")
self.remove_vehicle("VBAE_MMP")
self.remove_vehicle("AMX-30B2")
self.remove_vehicle("Tracma")
self.remove_vehicle("JTACFP")
self.remove_vehicle("SHERIDAN")
self.remove_vehicle("Leclerc_XXI")
self.remove_vehicle("Toyota_bleu")
self.remove_vehicle("Toyota_vert")
self.remove_vehicle("Toyota_desert")
self.remove_vehicle("Kamikaze")
self.remove_vehicle("AMX1375")
self.remove_vehicle("AMX1390")
self.remove_vehicle("VBCI")
self.remove_vehicle("T62")
self.remove_vehicle("T64BV")
self.remove_vehicle("T72M")
self.remove_vehicle("KORNET")
# high digit sams
if not mod_settings.high_digit_sams:
self.remove_preset("SA-10B/S-300PS")
self.remove_preset("SA-12/S-300V")
self.remove_preset("SA-20/S-300PMU-1")
self.remove_preset("SA-20B/S-300PMU-2")
self.remove_preset("SA-23/S-300VM")
self.remove_preset("SA-17")
self.remove_preset("KS-19_HDS")
self.remove_preset("HQ-2")
self.remove_preset("SA-2/S-75 V-759/5V23")
self.remove_preset("SA-3/S-125 V-601P/5V27")
self.remove_vehicle("SAM SA-14 Strela-3 manpad")
self.remove_vehicle("SAM SA-24 Igla-S manpad")
self.remove_vehicle("Polyana-D4M1 C2 node")
if not mod_settings.fa_18efg:
self.remove_aircraft("FA-18E")
self.remove_aircraft("FA-18F")
self.remove_aircraft("EA-18G")
# spanish naval assets pack
if not mod_settings.spanishnavypack:
self.remove_ship("L61")
self.remove_ship("F100")
self.remove_ship("F105")
self.remove_ship("L52")
self.remove_ship("L02")
self.remove_ship("DDG39")
if not mod_settings.irondome:
self.remove_vehicle("I9K51_GRAD")
self.remove_vehicle("I9K57_URAGAN")
self.remove_vehicle("I9K58_SMERCH")
self.remove_vehicle("IRON_DOME_CP")
self.remove_vehicle("IRON_DOME_LN")
self.remove_vehicle("ELM2048_MMR")
self.remove_preset("Iron Dome")
# swedish military assets pack
if not mod_settings.swedishmilitaryassetspack:
self.remove_vehicle("BV410_RBS70")
self.remove_vehicle("BV410_RBS90")
self.remove_vehicle("LvS_103_Lavett103_Rb103A")
self.remove_vehicle("LvS_103_Lavett103_Rb103B")
self.remove_vehicle("LvS_103_Lavett103_HX_Rb103A")
self.remove_vehicle("LvS_103_Lavett103_HX_Rb103B")
self.remove_vehicle("LvS_103_StriE103")
self.remove_vehicle("LvS_103_PM103")
self.remove_vehicle("LvS_103_PM103_HX")
self.remove_vehicle("LvS_103_Elverk103")
self.remove_vehicle("LvKv9040")
self.remove_vehicle("RBS_70")
self.remove_vehicle("RBS_90")
self.remove_vehicle("RBS_98")
self.remove_vehicle("UndE23")
self.remove_vehicle("BV410")
self.remove_vehicle("CV9040")
self.remove_vehicle("Strv103")
self.remove_vehicle("Strv121")
self.remove_vehicle("Strv122")
self.remove_vehicle("Strv2000")
self.remove_vehicle("Volvo740")
self.remove_vehicle("RBS_15KA")
self.remove_vehicle("AG_90")
self.remove_vehicle("SwedishinfantryAK4")
self.remove_vehicle("SwedishinfantryAK5")
self.remove_vehicle("SwedishinfantryAK5GT")
self.remove_vehicle("SwedishinfantryKSP90")
self.remove_vehicle("SwedishinfantryKSP58")
self.remove_vehicle("SwedishinfantryPskott86")
self.remove_vehicle("RBS_57")
self.remove_vehicle("RBS_58")
self.remove_vehicle("Artillerisystem08")
self.remove_vehicle("Grkpbv90")
self.remove_ship("HSwMS_Visby")
self.remove_ship("Strb90")
self.remove_aircraft("HKP15B")
# SWPack
if not mod_settings.SWPack:
self.remove_aircraft("AWINGA")
self.remove_aircraft("AWING")
self.remove_aircraft("XWING")
self.remove_aircraft("XWINGAI")
self.remove_aircraft("TIE_BA")
self.remove_aircraft("tie_bomber_2")
self.remove_aircraft("YWINGA")
self.remove_aircraft("YWING")
self.remove_aircraft("CORVETTE")
self.remove_aircraft("CORVETTEA")
self.remove_aircraft("FAUCON")
self.remove_aircraft("FAUCON_AI")
self.remove_aircraft("TIE")
self.remove_aircraft("TIE_AI")
self.remove_aircraft("HUNTER")
self.remove_aircraft("HUNTERA")
self.remove_aircraft("TIE_INTER")
self.remove_aircraft("TIE_INTERA")
self.remove_aircraft("naboo_starfighter")
self.remove_aircraft("naboo_starfighter_AI")
self.remove_vehicle("MBT9_REBEL")
self.remove_vehicle("MBT9_AAA EMPIRE")
self.remove_vehicle("MBT9_EMPIRE")
self.remove_vehicle("MBT9_AAA REBEL")
self.remove_vehicle("Jugger")
self.remove_vehicle("TB_TT")
self.remove_vehicle("TR_TT")
self.remove_vehicle("Gozanti")
self.remove_ship("Destroyer_carrier")
def remove_aircraft(self, name: str) -> None:
for i in list(self.aircrafts):
if i.dcs_unit_type.id == name:
self.aircrafts.remove(i)
def remove_aircraft_by_name(self, name: str) -> None:
for i in list(self.aircrafts):
if i.name == name:
self.aircrafts.remove(i)
def remove_preset(self, name: str) -> None:
for pg in self.preset_groups:
if pg.name == name:
self.preset_groups.remove(pg)
def remove_vehicle(self, name: str) -> None:
for sequence in [
self.frontline_units,
self.infantry_units,
self.air_defense_units,
self.artillery_units,
self.logistics_units,
]:
for i in list(sequence):
if i.dcs_unit_type.id == name:
sequence.remove(i)
def remove_ship(self, name: str) -> None:
for i in list(self.naval_units):
if i.dcs_unit_type.id == name:
self.naval_units.remove(i)
def load_ship(name: str) -> Optional[Type[ShipType]]:
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[str]) -> List[Type[ShipType]]:
items = []
for name in data:
item = load_ship(name)
if item is not None:
items.append(item)
return items