From e8aa9839b01125b62d795a938cfc0109aa3be4f3 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 27 Nov 2020 17:23:17 -0800 Subject: [PATCH] Fall back to lower range SAMs when needed. Mostly fixes https://github.com/Khopa/dcs_liberation/issues/473. The last part of the fix is to migrate the `shorads` property of the faction to just be in `sams` and just use the property to decide its use. Currently factions like USA 2005 that have long range SAMs and SHORADs only will still not spawn anything at medium sites because they have no other SAMs declared. --- game/theater/start_generator.py | 43 +++++++++++-------- gen/sam/sam_group_generator.py | 74 ++++++++++++++++++++------------- 2 files changed, 71 insertions(+), 46 deletions(-) diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 0c4bec24..ea0331c0 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -4,7 +4,7 @@ import logging import math import pickle import random -from typing import Any, Dict, Iterable, Optional +from typing import Any, Dict, Iterable, Optional, Set from dcs.mapping import Point from dcs.task import CAP, CAS, PinpointStrike @@ -35,9 +35,11 @@ from gen.fleet.ship_group_generator import ( ) from gen.locations.preset_location_finder import MizDataLocationFinder from gen.missiles.missiles_group_generator import generate_missile_group +from gen.sam.airdefensegroupgenerator import AirDefenseRange from gen.sam.sam_group_generator import ( - LONG_RANGE_SAMS, MEDIUM_RANGE_SAMS, generate_anti_air_group, - generate_ewr_group, generate_shorad_group, + generate_anti_air_group, + generate_ewr_group, + generate_shorad_group, ) from . import ( ConflictTheater, @@ -585,9 +587,16 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): """ presets = self.control_point.preset_locations for position in presets.required_long_range_sams: - self.generate_aa_at(position, filter_names=LONG_RANGE_SAMS) + self.generate_aa_at(position, ranges=[ + {AirDefenseRange.Long}, + {AirDefenseRange.Medium}, + {AirDefenseRange.Short}, + ]) for position in presets.required_medium_range_sams: - self.generate_aa_at(position, filter_names=MEDIUM_RANGE_SAMS) + self.generate_aa_at(position, ranges=[ + {AirDefenseRange.Medium}, + {AirDefenseRange.Short}, + ]) return (len(presets.required_long_range_sams) + len(presets.required_medium_range_sams)) @@ -629,25 +638,23 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): position = self.location_finder.location_for(LocationType.Sam) if position is None: return - self.generate_aa_at(position) + self.generate_aa_at(position, ranges=[ + # Prefer to use proper SAMs, but fall back to SHORADs if needed. + {AirDefenseRange.Long, AirDefenseRange.Medium}, + {AirDefenseRange.Short}, + ]) - def generate_aa_at(self, position: Point, - filter_names: Optional[Iterable[str]] = None) -> None: + def generate_aa_at( + self, position: Point, + ranges: Iterable[Set[AirDefenseRange]]) -> None: group_id = self.game.next_group_id() g = SamGroundObject(namegen.random_objective_name(), group_id, position, self.control_point, for_airbase=False) - group = generate_anti_air_group(self.game, g, self.faction, - filter_names) + group = generate_anti_air_group(self.game, g, self.faction, ranges) if group is None: - location = f"{g.name} at {self.control_point}" - if filter_names is not None: - logging.warning( - "Could not generate SAM group for %s from types: %s", - location, ", ".join(filter_names) - ) - else: - logging.error("Could not generate SAM group for %s", location) + logging.error("Could not generate air defense group for %s at %s", + g.name, self.control_point) return g.groups = [group] self.control_point.connected_objectives.append(g) diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index 343a462d..3b678467 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -1,5 +1,5 @@ import random -from typing import Dict, Iterable, List, Optional, Type +from typing import Dict, Iterable, List, Optional, Sequence, Set, Type from dcs.unitgroup import VehicleGroup from dcs.vehicles import AirDefence @@ -13,6 +13,10 @@ from gen.sam.aaa_flak import FlakGenerator from gen.sam.aaa_flak18 import Flak18Generator from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator +from gen.sam.airdefensegroupgenerator import ( + AirDefenseGroupGenerator, + AirDefenseRange, +) from gen.sam.cold_war_flak import ( ColdWarFlakGenerator, EarlyColdWarFlakGenerator, @@ -30,7 +34,6 @@ from gen.sam.ewrs import ( TallRackGenerator, ) from gen.sam.freya_ewr import FreyaGenerator -from gen.sam.airdefensegroupgenerator import AirDefenseGroupGenerator from gen.sam.group_generator import GroupGenerator from gen.sam.sam_avenger import AvengerGenerator from gen.sam.sam_chaparral import ChaparralGenerator @@ -98,17 +101,6 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = { "AllyWW2FlakGenerator": AllyWW2FlakGenerator } -#: Used to fill the long-range required SAM locations in the campaign. -LONG_RANGE_SAMS = { - "SA10Generator", - "PatriotGenerator", - "Tier2SA10Generator", - "Tier3SA10Generator", -} - -#: Used to fill the medium-range required SAM location in the campaign. -MEDIUM_RANGE_SAMS = SAM_MAP.keys() - LONG_RANGE_SAMS - SAM_PRICES = { AirDefence.SAM_Hawk_PCP: 35, @@ -159,16 +151,12 @@ EWR_MAP = { def get_faction_possible_sams_generator( - faction: Faction, - filter_names: Optional[Iterable[str]] = None -) -> List[Type[GroupGenerator]]: + faction: Faction) -> List[Type[AirDefenseGroupGenerator]]: """ Return the list of possible SAM generator for the given faction :param faction: Faction name to search units for - :param filter_names: Optional list of names to filter allowed SAMs by. """ - return [SAM_MAP[s] for s in faction.sams if - filter_names is None or s in filter_names] + return [SAM_MAP[s] for s in faction.sams] def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGenerator]]: @@ -179,23 +167,53 @@ def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGene return [EWR_MAP[s] for s in faction.ewrs] +def _generate_anti_air_from( + generators: Sequence[Type[AirDefenseGroupGenerator]], game: Game, + ground_object: SamGroundObject) -> Optional[VehicleGroup]: + if not generators: + return None + sam_generator_class = random.choice(generators) + generator = sam_generator_class(game, ground_object) + generator.generate() + return generator.get_generated_group() + + def generate_anti_air_group( - game: Game, ground_object: TheaterGroundObject, faction: Faction, - filter_names: Optional[Iterable[str]] = None) -> Optional[VehicleGroup]: + game: Game, ground_object: SamGroundObject, faction: Faction, + ranges: Optional[Iterable[Set[AirDefenseRange]]] = None +) -> Optional[VehicleGroup]: """ This generate a SAM group :param game: The Game. :param ground_object: The ground object which will own the sam group. :param faction: Owner faction. - :param filter_names: Optional list of names to filter allowed SAMs by. + :param ranges: Optional list of preferred ranges of the air defense to + create. If None, any generator may be used. Otherwise generators + matching the given ranges will be used in order of preference. For + example, when given `[{Long, Medium}, {Short}]`, long and medium range + air defenses will be tried first with no bias, and short range air + defenses will be used if no long or medium range generators are + available to the faction. If instead `[{Long}, {Medium}, {Short}]` had + been used, long range systems would take precedence over medium range + systems. If instead `[{Long, Medium, Short}]` had been used, all types + would be considered with equal preference. :return: The generated group, or None if one could not be generated. """ - generators = get_faction_possible_sams_generator(faction, filter_names) - if len(generators) > 0: - sam_generator_class = random.choice(generators) - generator = sam_generator_class(game, ground_object) - generator.generate() - return generator.get_generated_group() + generators = get_faction_possible_sams_generator(faction) + if ranges is None: + ranges = [{ + AirDefenseRange.Long, + AirDefenseRange.Medium, + AirDefenseRange.Short, + }] + + for range_options in ranges: + generators_for_range = [g for g in generators if + g.range() in range_options] + group = _generate_anti_air_from(generators_for_range, game, + ground_object) + if group is not None: + return group return None