Remove random objective generation.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1076
This commit is contained in:
Dan Albert 2021-05-30 20:37:51 -07:00
parent d1c7146a47
commit 871e7f7a50
6 changed files with 91 additions and 593 deletions

View File

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
import logging
import math import math
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
@ -40,9 +39,6 @@ from dcs.unitgroup import (
VehicleGroup, VehicleGroup,
) )
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from .latlon import LatLon
from ..scenery_group import SceneryGroup
from pyproj import CRS, Transformer from pyproj import CRS, Transformer
from shapely import geometry, ops from shapely import geometry, ops
@ -57,10 +53,12 @@ from .controlpoint import (
) )
from .frontline import FrontLine from .frontline import FrontLine
from .landmap import Landmap, load_landmap, poly_contains from .landmap import Landmap, load_landmap, poly_contains
from .latlon import LatLon
from .projections import TransverseMercator from .projections import TransverseMercator
from ..point_with_heading import PointWithHeading from ..point_with_heading import PointWithHeading
from ..profiling import logged_duration from ..profiling import logged_duration
from ..utils import Distance, meters, nautical_miles from ..scenery_group import SceneryGroup
from ..utils import Distance, meters
SIZE_TINY = 150 SIZE_TINY = 150
SIZE_SMALL = 600 SIZE_SMALL = 600
@ -87,42 +85,39 @@ class MizCampaignLoader:
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
FARP_HELIPAD = "SINGLE_HELIPAD" FARP_HELIPAD = "SINGLE_HELIPAD"
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_Grison.id
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id
MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id
# Multiple options for the required SAMs so campaign designers can more # Multiple options for air defenses so campaign designers can more accurately see
# accurately see the coverage of their IADS for the expected type. # the coverage of their IADS for the expected type.
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = { LONG_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Patriot_LN.id, AirDefence.SAM_Patriot_LN.id,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id, AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id, AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id,
} }
REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = { MEDIUM_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Hawk_LN_M192.id, AirDefence.SAM_Hawk_LN_M192.id,
AirDefence.SAM_SA_2_S_75_Guideline_LN.id, AirDefence.SAM_SA_2_S_75_Guideline_LN.id,
AirDefence.SAM_SA_3_S_125_Goa_LN.id, AirDefence.SAM_SA_3_S_125_Goa_LN.id,
} }
REQUIRED_SHORT_RANGE_SAM_UNIT_TYPES = { SHORT_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Avenger__Stinger.id, AirDefence.SAM_Avenger__Stinger.id,
AirDefence.SAM_Rapier_LN.id, AirDefence.SAM_Rapier_LN.id,
AirDefence.SAM_SA_19_Tunguska_Grison.id, AirDefence.SAM_SA_19_Tunguska_Grison.id,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id, AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id,
} }
REQUIRED_AAA_UNIT_TYPES = { AAA_UNIT_TYPES = {
AirDefence.AAA_8_8cm_Flak_18.id, AirDefence.AAA_8_8cm_Flak_18.id,
AirDefence.SPAAA_Vulcan_M163.id, AirDefence.SPAAA_Vulcan_M163.id,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id, AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id,
} }
REQUIRED_EWR_UNIT_TYPE = AirDefence.EWR_1L13.id EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id
@ -130,9 +125,7 @@ class MizCampaignLoader:
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id
REQUIRED_STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
BASE_DEFENSE_RADIUS = nautical_miles(2)
def __init__(self, miz: Path, theater: ConflictTheater) -> None: def __init__(self, miz: Path, theater: ConflictTheater) -> None:
self.theater = theater self.theater = theater
@ -210,98 +203,56 @@ class MizCampaignLoader:
@property @property
def ships(self) -> Iterator[ShipGroup]: def ships(self) -> Iterator[ShipGroup]:
for group in self.blue.ship_group:
if group.units[0].type == self.SHIP_UNIT_TYPE:
yield group
@property
def required_ships(self) -> Iterator[ShipGroup]:
for group in self.red.ship_group: for group in self.red.ship_group:
if group.units[0].type == self.SHIP_UNIT_TYPE: if group.units[0].type == self.SHIP_UNIT_TYPE:
yield group yield group
@property
def ewrs(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.EWR_UNIT_TYPE:
yield group
@property
def sams(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.SAM_UNIT_TYPE:
yield group
@property
def garrisons(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.GARRISON_UNIT_TYPE:
yield group
@property @property
def offshore_strike_targets(self) -> Iterator[StaticGroup]: def offshore_strike_targets(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
yield group
@property
def required_offshore_strike_targets(self) -> Iterator[StaticGroup]:
for group in self.red.static_group: for group in self.red.static_group:
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE: if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
yield group yield group
@property @property
def missile_sites(self) -> Iterator[VehicleGroup]: def missile_sites(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
yield group
@property
def required_missile_sites(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: for group in self.red.vehicle_group:
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE: if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
yield group yield group
@property @property
def coastal_defenses(self) -> Iterator[VehicleGroup]: def coastal_defenses(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
yield group
@property
def required_coastal_defenses(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: for group in self.red.vehicle_group:
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE: if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
yield group yield group
@property @property
def required_long_range_sams(self) -> Iterator[VehicleGroup]: def long_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_LONG_RANGE_SAM_UNIT_TYPES: if group.units[0].type in self.LONG_RANGE_SAM_UNIT_TYPES:
yield group yield group
@property @property
def required_medium_range_sams(self) -> Iterator[VehicleGroup]: def medium_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES: if group.units[0].type in self.MEDIUM_RANGE_SAM_UNIT_TYPES:
yield group yield group
@property @property
def required_short_range_sams(self) -> Iterator[VehicleGroup]: def short_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_SHORT_RANGE_SAM_UNIT_TYPES: if group.units[0].type in self.SHORT_RANGE_SAM_UNIT_TYPES:
yield group yield group
@property @property
def required_aaa(self) -> Iterator[VehicleGroup]: def aaa(self) -> Iterator[VehicleGroup]:
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group): for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
if group.units[0].type in self.REQUIRED_AAA_UNIT_TYPES: if group.units[0].type in self.AAA_UNIT_TYPES:
yield group yield group
@property @property
def required_ewrs(self) -> Iterator[VehicleGroup]: def ewrs(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_EWR_UNIT_TYPE: if group.units[0].type in self.EWR_UNIT_TYPE:
yield group yield group
@property @property
@ -329,9 +280,9 @@ class MizCampaignLoader:
yield group yield group
@property @property
def required_strike_targets(self) -> Iterator[StaticGroup]: def strike_targets(self) -> Iterator[StaticGroup]:
for group in itertools.chain(self.blue.static_group, self.red.static_group): for group in itertools.chain(self.blue.static_group, self.red.static_group):
if group.units[0].type in self.REQUIRED_STRIKE_TARGET_UNIT_TYPE: if group.units[0].type in self.STRIKE_TARGET_UNIT_TYPE:
yield group yield group
@property @property
@ -440,112 +391,57 @@ class MizCampaignLoader:
return closest, distance return closest, distance
def add_preset_locations(self) -> None: def add_preset_locations(self) -> None:
for group in self.garrisons:
closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_garrisons.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
else:
logging.warning(f"Found garrison unit too far from base: {group.name}")
for group in self.sams:
closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_air_defense.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
else:
closest.preset_locations.strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.ewrs:
closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_ewrs.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
else:
closest.preset_locations.ewrs.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.offshore_strike_targets: for group in self.offshore_strike_targets:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.offshore_strike_locations.append( closest.preset_locations.offshore_strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.required_offshore_strike_targets:
closest, distance = self.objective_info(group)
closest.preset_locations.required_offshore_strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.ships: for group in self.ships:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.ships.append( closest.preset_locations.ships.append(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.required_ships:
closest, distance = self.objective_info(group)
closest.preset_locations.required_ships.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.missile_sites: for group in self.missile_sites:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.missile_sites.append( closest.preset_locations.missile_sites.append(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.required_missile_sites:
closest, distance = self.objective_info(group)
closest.preset_locations.required_missile_sites.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.coastal_defenses: for group in self.coastal_defenses:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.coastal_defenses.append( closest.preset_locations.coastal_defenses.append(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.required_coastal_defenses: for group in self.long_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.required_coastal_defenses.append( closest.preset_locations.long_range_sams.append(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.required_long_range_sams: for group in self.medium_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.required_long_range_sams.append( closest.preset_locations.medium_range_sams.append(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.required_medium_range_sams: for group in self.short_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.required_medium_range_sams.append( closest.preset_locations.short_range_sams.append(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.required_short_range_sams: for group in self.aaa:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.required_short_range_sams.append( closest.preset_locations.aaa.append(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.required_aaa: for group in self.ewrs:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.required_aaa.append( closest.preset_locations.ewrs.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_ewrs:
closest, distance = self.objective_info(group)
closest.preset_locations.required_ewrs.append(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
@ -573,9 +469,9 @@ class MizCampaignLoader:
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
for group in self.required_strike_targets: for group in self.strike_targets:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.required_strike_locations.append( closest.preset_locations.strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import heapq import heapq
import itertools import itertools
import logging import logging
import random
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -44,12 +43,8 @@ from gen.runways import RunwayAssigner, RunwayData
from .base import Base from .base import Base
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .theatergroundobject import ( from .theatergroundobject import (
BaseDefenseGroundObject,
EwrGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,
SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
VehicleGroupGroundObject,
) )
from ..db import PRICES from ..db import PRICES
from ..utils import nautical_miles from ..utils import nautical_miles
@ -78,139 +73,55 @@ class ControlPointType(Enum):
OFF_MAP = 6 OFF_MAP = 6
class LocationType(Enum):
BaseAirDefense = "base air defense"
Coastal = "coastal defense"
Ewr = "EWR"
BaseEwr = "Base EWR"
Garrison = "garrison"
MissileSite = "missile site"
OffshoreStrikeTarget = "offshore strike target"
Sam = "SAM"
Ship = "ship"
Shorad = "SHORAD"
StrikeTarget = "strike target"
@dataclass @dataclass
class PresetLocations: class PresetLocations:
"""Defines the preset locations loaded from the campaign mission file.""" """Defines the preset locations loaded from the campaign mission file."""
#: Locations used for spawning ground defenses for bases. #: Locations used by non-carrier ships that will be spawned unless the faction has
base_garrisons: List[PointWithHeading] = field(default_factory=list) #: no navy or the player has disabled ship generation for the owning side.
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
#: and SHORADs.
base_air_defense: List[PointWithHeading] = field(default_factory=list)
#: Locations used by EWRs.
ewrs: List[PointWithHeading] = field(default_factory=list)
#: Locations used by Base EWRs.
base_ewrs: List[PointWithHeading] = field(default_factory=list)
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
ships: List[PointWithHeading] = field(default_factory=list) ships: List[PointWithHeading] = field(default_factory=list)
#: Locations used by non-carrier ships that will be spawned unless the faction has #: Locations used by coastal defenses that are generated if the faction is capable.
#: no navy or the player has disable ship generation for the original owning side.
required_ships: List[PointWithHeading] = field(default_factory=list)
#: Locations used by coastal defenses.
coastal_defenses: List[PointWithHeading] = field(default_factory=list) coastal_defenses: List[PointWithHeading] = field(default_factory=list)
#: Locations used by coastal defenses that are always generated if the faction is
#: capable.
required_coastal_defenses: List[PointWithHeading] = field(default_factory=list)
#: Locations used by ground based strike objectives. #: Locations used by ground based strike objectives.
strike_locations: List[PointWithHeading] = field(default_factory=list) strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by ground based strike objectives that will always be spawned.
required_strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by offshore strike objectives. #: Locations used by offshore strike objectives.
offshore_strike_locations: List[PointWithHeading] = field(default_factory=list) offshore_strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by offshore strike objectives that will always be spawned. #: Locations used by missile sites like scuds and V-2s that are generated if the
required_offshore_strike_locations: List[PointWithHeading] = field( #: faction is capable.
default_factory=list
)
#: Locations used by missile sites like scuds and V-2s.
missile_sites: List[PointWithHeading] = field(default_factory=list) missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations used by missile sites like scuds and V-2s that are always generated if #: Locations of long range SAMs.
#: the faction is capable. long_range_sams: List[PointWithHeading] = field(default_factory=list)
required_missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations of long range SAMs which should always be spawned. #: Locations of medium range SAMs.
required_long_range_sams: List[PointWithHeading] = field(default_factory=list) medium_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of medium range SAMs which should always be spawned. #: Locations of short range SAMs.
required_medium_range_sams: List[PointWithHeading] = field(default_factory=list) short_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of short range SAMs which should always be spawned. #: Locations of AAA groups.
required_short_range_sams: List[PointWithHeading] = field(default_factory=list) aaa: List[PointWithHeading] = field(default_factory=list)
#: Locations of AAA groups which should always be spawned. #: Locations of EWRs.
required_aaa: List[PointWithHeading] = field(default_factory=list) ewrs: List[PointWithHeading] = field(default_factory=list)
#: Locations of EWRs which should always be spawned.
required_ewrs: List[PointWithHeading] = field(default_factory=list)
#: Locations of map scenery to create zones for. #: Locations of map scenery to create zones for.
scenery: List[SceneryGroup] = field(default_factory=list) scenery: List[SceneryGroup] = field(default_factory=list)
#: Locations of factories for producing ground units. These will always be spawned. #: Locations of factories for producing ground units.
factories: List[PointWithHeading] = field(default_factory=list) factories: List[PointWithHeading] = field(default_factory=list)
#: Locations of ammo depots for controlling number of units on the front line at a control point. #: Locations of ammo depots for controlling number of units on the front line at a
#: control point.
ammunition_depots: List[PointWithHeading] = field(default_factory=list) ammunition_depots: List[PointWithHeading] = field(default_factory=list)
#: Locations of stationary armor groups. These will always be spawned. #: Locations of stationary armor groups.
armor_groups: List[PointWithHeading] = field(default_factory=list) armor_groups: List[PointWithHeading] = field(default_factory=list)
@staticmethod
def _random_from(points: List[PointWithHeading]) -> Optional[PointWithHeading]:
"""Finds, removes, and returns a random position from the given list."""
if not points:
return None
point = random.choice(points)
points.remove(point)
return point
def random_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
"""Returns a position suitable for the given location type.
The location, if found, will be claimed by the caller and not available
to subsequent calls.
"""
if location_type == LocationType.BaseAirDefense:
return self._random_from(self.base_air_defense)
if location_type == LocationType.Coastal:
return self._random_from(self.coastal_defenses)
if location_type == LocationType.Ewr:
return self._random_from(self.ewrs)
if location_type == LocationType.BaseEwr:
return self._random_from(self.base_ewrs)
if location_type == LocationType.Garrison:
return self._random_from(self.base_garrisons)
if location_type == LocationType.MissileSite:
return self._random_from(self.missile_sites)
if location_type == LocationType.OffshoreStrikeTarget:
return self._random_from(self.offshore_strike_locations)
if location_type == LocationType.Sam:
return self._random_from(self.strike_locations)
if location_type == LocationType.Ship:
return self._random_from(self.ships)
if location_type == LocationType.Shorad:
return self._random_from(self.base_garrisons)
if location_type == LocationType.StrikeTarget:
return self._random_from(self.strike_locations)
logging.error(f"Unknown location type: {location_type}")
return None
@dataclass(frozen=True) @dataclass(frozen=True)
class PendingOccupancy: class PendingOccupancy:
@ -338,7 +249,6 @@ class ControlPoint(MissionTarget, ABC):
self.full_name = name self.full_name = name
self.at = at self.at = at
self.connected_objectives: List[TheaterGroundObject] = [] self.connected_objectives: List[TheaterGroundObject] = []
self.base_defenses: List[BaseDefenseGroundObject] = []
self.preset_locations = PresetLocations() self.preset_locations = PresetLocations()
self.helipads: List[PointWithHeading] = [] self.helipads: List[PointWithHeading] = []
@ -367,7 +277,7 @@ class ControlPoint(MissionTarget, ABC):
@property @property
def ground_objects(self) -> List[TheaterGroundObject]: def ground_objects(self) -> List[TheaterGroundObject]:
return list(itertools.chain(self.connected_objectives, self.base_defenses)) return list(self.connected_objectives)
@property @property
@abstractmethod @abstractmethod
@ -553,24 +463,6 @@ class ControlPoint(MissionTarget, ABC):
def is_friendly_to(self, control_point: ControlPoint) -> bool: def is_friendly_to(self, control_point: ControlPoint) -> bool:
return control_point.is_friendly(self.captured) return control_point.is_friendly(self.captured)
# TODO: Should be Airbase specific.
def clear_base_defenses(self) -> None:
for base_defense in self.base_defenses:
p = PointWithHeading.from_point(base_defense.position, base_defense.heading)
if isinstance(base_defense, EwrGroundObject):
self.preset_locations.base_ewrs.append(p)
elif isinstance(base_defense, SamGroundObject):
self.preset_locations.base_air_defense.append(p)
elif isinstance(base_defense, VehicleGroupGroundObject):
self.preset_locations.base_garrisons.append(p)
else:
logging.error(
"Could not determine preset location type for "
f"{base_defense}. Assuming garrison type."
)
self.preset_locations.base_garrisons.append(p)
self.base_defenses = []
def capture_equipment(self, game: Game) -> None: def capture_equipment(self, game: Game) -> None:
total = self.base.total_armor_value total = self.base.total_armor_value
self.base.armor.clear() self.base.armor.clear()
@ -668,11 +560,6 @@ class ControlPoint(MissionTarget, ABC):
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()
self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator
BaseDefenseGenerator(game, self).generate()
@abstractmethod @abstractmethod
def can_operate(self, aircraft: Type[FlyingType]) -> bool: def can_operate(self, aircraft: Type[FlyingType]) -> bool:
... ...

View File

@ -1,12 +1,11 @@
from __future__ import annotations from __future__ import annotations
from game.scenery_group import SceneryGroup
import logging import logging
import pickle import pickle
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Any, Dict, Iterable, List, Optional, Set from typing import Any, Dict, Iterable, List, Set
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike from dcs.task import CAP, CAS, PinpointStrike
@ -14,7 +13,8 @@ from dcs.vehicles import AirDefence
from game import Game, db from game import Game, db
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater import Carrier, Lha, LocationType, PointWithHeading from game.scenery_group import SceneryGroup
from game.theater import Carrier, Lha, PointWithHeading
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
CarrierGroundObject, CarrierGroundObject,
@ -39,8 +39,8 @@ from gen.fleet.ship_group_generator import (
) )
from gen.missiles.missiles_group_generator import generate_missile_group from gen.missiles.missiles_group_generator import generate_missile_group
from gen.sam.airdefensegroupgenerator import AirDefenseRange from gen.sam.airdefensegroupgenerator import AirDefenseRange
from gen.sam.sam_group_generator import generate_anti_air_group
from gen.sam.ewr_group_generator import generate_ewr_group from gen.sam.ewr_group_generator import generate_ewr_group
from gen.sam.sam_group_generator import generate_anti_air_group
from . import ( from . import (
ConflictTheater, ConflictTheater,
ControlPoint, ControlPoint,
@ -145,24 +145,6 @@ class GameGenerator:
cp.captured = True cp.captured = True
class LocationFinder:
def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point
def location_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
position = self.control_point.preset_locations.random_for(location_type)
if position is not None:
logging.warning(
f"Campaign relies on random generation of %s at %s. Support for random "
"objectives will be removed soon.",
location_type.value,
self.control_point,
)
return position
return None
class ControlPointGroundObjectGenerator: class ControlPointGroundObjectGenerator:
def __init__( def __init__(
self, self,
@ -173,7 +155,6 @@ class ControlPointGroundObjectGenerator:
self.game = game self.game = game
self.generator_settings = generator_settings self.generator_settings = generator_settings
self.control_point = control_point self.control_point = control_point
self.location_finder = LocationFinder(control_point)
@property @property
def faction_name(self) -> str: def faction_name(self) -> str:
@ -203,19 +184,9 @@ class ControlPointGroundObjectGenerator:
if not self.control_point.captured and skip_enemy_navy: if not self.control_point.captured and skip_enemy_navy:
return return
self.generate_required_ships() for position in self.control_point.preset_locations.ships:
for _ in range(self.faction.navy_group_count):
self.generate_ship()
def generate_required_ships(self) -> None:
for position in self.control_point.preset_locations.required_ships:
self.generate_ship_at(position) self.generate_ship_at(position)
def generate_ship(self) -> None:
point = self.location_finder.location_for(LocationType.Ship)
if point is not None:
self.generate_ship_at(point)
def generate_ship_at(self, position: PointWithHeading) -> None: def generate_ship_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -289,159 +260,6 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
return True return True
class BaseDefenseGenerator:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
self.location_finder = LocationFinder(control_point)
@property
def faction_name(self) -> str:
if self.control_point.captured:
return self.game.player_name
else:
return self.game.enemy_name
@property
def faction(self) -> Faction:
return db.FACTIONS[self.faction_name]
def generate(self) -> None:
self.generate_ewr()
self.generate_garrison()
self.generate_base_defenses()
def generate_ewr(self) -> None:
position = self.location_finder.location_for(LocationType.BaseEwr)
if position is None:
return
group_id = self.game.next_group_id()
g = EwrGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
True,
)
group = generate_ewr_group(self.game, g, self.faction)
if group is None:
logging.error(f"Could not generate EWR at {self.control_point}")
return
g.groups = [group]
self.control_point.base_defenses.append(g)
def generate_base_defenses(self) -> None:
# First group has a 1/2 chance of being a SAM, 1/6 chance of SHORAD,
# and a 1/6 chance of a garrison.
#
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
# being a garrison.
for i in range(random.randint(2, 5)):
if i == 0 and random.randint(0, 1) == 0:
self.generate_sam()
elif random.randint(0, 2) == 1:
self.generate_shorad()
else:
self.generate_garrison()
def generate_garrison(self) -> None:
position = self.location_finder.location_for(LocationType.Garrison)
if position is None:
return
group_id = self.game.next_group_id()
g = VehicleGroupGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
group = generate_armor_group(self.faction_name, self.game, g)
if group is None:
logging.error(f"Could not generate garrison at {self.control_point}")
return
g.groups.append(group)
self.control_point.base_defenses.append(g)
def generate_sam(self) -> None:
position = self.location_finder.location_for(LocationType.BaseAirDefense)
if position is None:
return
group_id = self.game.next_group_id()
g = SamGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
groups = generate_anti_air_group(self.game, g, self.faction)
if not groups:
logging.error(f"Could not generate SAM at {self.control_point}")
return
g.groups = groups
self.control_point.base_defenses.append(g)
def generate_shorad(self) -> None:
position = self.location_finder.location_for(LocationType.BaseAirDefense)
if position is None:
return
group_id = self.game.next_group_id()
g = SamGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
groups = generate_anti_air_group(
self.game,
g,
self.faction,
ranges=[{AirDefenseRange.Short, AirDefenseRange.AAA}],
)
if not groups:
logging.error(f"Could not generate SHORAD group at {self.control_point}")
return
g.groups = groups
self.control_point.base_defenses.append(g)
class FobDefenseGenerator(BaseDefenseGenerator):
def generate(self) -> None:
self.generate_garrison()
self.generate_fob_defenses()
def generate_fob_defenses(self):
# First group has a 1/2 chance of being a SHORAD,
# and a 1/2 chance of a garrison.
#
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
# being a garrison.
for i in range(random.randint(2, 5)):
if i == 0 and random.randint(0, 1) == 0:
self.generate_shorad()
elif i == 0 and random.randint(0, 1) == 0:
self.generate_garrison()
elif random.randint(0, 2) == 1:
self.generate_shorad()
else:
self.generate_garrison()
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__( def __init__(
self, self,
@ -457,16 +275,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not super().generate(): if not super().generate():
return False return False
BaseDefenseGenerator(self.game, self.control_point).generate()
self.generate_ground_points() self.generate_ground_points()
return True return True
def generate_ground_points(self) -> None: def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point.""" """Generate ground objects and AA sites for the control point."""
self.generate_armor_groups() self.generate_armor_groups()
skip_sams = self.generate_required_aa() self.generate_aa()
skip_ewrs = self.generate_required_ewr() self.generate_ewrs()
self.generate_scenery_sites() self.generate_scenery_sites()
self.generate_strike_targets() self.generate_strike_targets()
self.generate_offshore_strike_targets() self.generate_offshore_strike_targets()
@ -475,35 +291,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
if self.faction.missiles: if self.faction.missiles:
self.generate_missile_sites() self.generate_missile_sites()
self.generate_required_missile_sites()
if self.faction.coastal_defenses: if self.faction.coastal_defenses:
self.generate_coastal_sites() self.generate_coastal_sites()
self.generate_required_coastal_sites()
if self.control_point.is_global:
return
# Always generate at least one AA point.
self.generate_aa_site()
# And between 2 and 7 other objectives.
amount = random.randrange(2, 7)
for i in range(amount):
# 1 in 4 additional objectives are AA.
if random.randint(0, 3) == 0:
if skip_sams > 0:
skip_sams -= 1
else:
self.generate_aa_site()
# 1 in 4 additional objectives are EWR.
elif random.randint(0, 3) == 0:
if skip_ewrs > 0:
skip_ewrs -= 1
else:
self.generate_ewr_site()
else:
self.generate_ground_point()
def generate_armor_groups(self) -> None: def generate_armor_groups(self) -> None:
for position in self.control_point.preset_locations.armor_groups: for position in self.control_point.preset_locations.armor_groups:
@ -531,14 +321,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
g.groups = [group] g.groups = [group]
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
def generate_required_aa(self) -> int: def generate_aa(self) -> None:
"""Generates the AA sites that are required by the campaign.
Returns:
The number of AA sites that were generated.
"""
presets = self.control_point.preset_locations presets = self.control_point.preset_locations
for position in presets.required_long_range_sams: for position in presets.long_range_sams:
self.generate_aa_at( self.generate_aa_at(
position, position,
ranges=[ ranges=[
@ -548,7 +333,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
{AirDefenseRange.AAA}, {AirDefenseRange.AAA},
], ],
) )
for position in presets.required_medium_range_sams: for position in presets.medium_range_sams:
self.generate_aa_at( self.generate_aa_at(
position, position,
ranges=[ ranges=[
@ -557,52 +342,21 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
{AirDefenseRange.AAA}, {AirDefenseRange.AAA},
], ],
) )
for position in presets.required_short_range_sams: for position in presets.short_range_sams:
self.generate_aa_at( self.generate_aa_at(
position, position,
ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}], ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}],
) )
for position in presets.required_aaa: for position in presets.aaa:
self.generate_aa_at( self.generate_aa_at(
position, position,
ranges=[{AirDefenseRange.AAA}], ranges=[{AirDefenseRange.AAA}],
) )
return (
len(presets.required_long_range_sams)
+ len(presets.required_medium_range_sams)
+ len(presets.required_short_range_sams)
+ len(presets.required_aaa)
)
def generate_required_ewr(self) -> int: def generate_ewrs(self) -> None:
"""Generates the EWR sites that are required by the campaign.
Returns:
The number of EWR sites that were generated.
"""
presets = self.control_point.preset_locations presets = self.control_point.preset_locations
for position in presets.required_ewrs: for position in presets.ewrs:
self.generate_ewr_at(position) self.generate_ewr_at(position)
return len(presets.required_ewrs)
def generate_ground_point(self) -> None:
try:
category = random.choice(self.faction.building_set)
except IndexError:
logging.exception("Faction has no buildings defined")
return
if category == "oil":
location_type = LocationType.OffshoreStrikeTarget
else:
location_type = LocationType.StrikeTarget
# Pick from preset locations
point = self.location_finder.location_for(location_type)
if point is None:
return
self.generate_strike_target_at(category, point)
def generate_strike_target_at(self, category: str, position: Point) -> None: def generate_strike_target_at(self, category: str, position: Point) -> None:
@ -635,7 +389,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.generate_strike_target_at(category="ammo", position=position) self.generate_strike_target_at(category="ammo", position=position)
def generate_factories(self) -> None: def generate_factories(self) -> None:
"""Generates the factories that are required by the campaign."""
for position in self.control_point.preset_locations.factories: for position in self.control_point.preset_locations.factories:
self.generate_factory_at(position) self.generate_factory_at(position)
@ -653,19 +406,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
def generate_aa_site(self) -> None:
position = self.location_finder.location_for(LocationType.Sam)
if position is None:
return
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( def generate_aa_at(
self, position: Point, ranges: Iterable[Set[AirDefenseRange]] self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
) -> None: ) -> None:
@ -689,12 +429,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
g.groups = groups g.groups = groups
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
def generate_ewr_site(self) -> None:
position = self.location_finder.location_for(LocationType.Ewr)
if position is None:
return
self.generate_ewr_at(position)
def generate_ewr_at(self, position: PointWithHeading) -> None: def generate_ewr_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -750,18 +484,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return return
def generate_required_missile_sites(self) -> None:
for position in self.control_point.preset_locations.required_missile_sites:
self.generate_missile_site_at(position)
def generate_missile_sites(self) -> None: def generate_missile_sites(self) -> None:
for i in range(self.faction.missiles_group_count): for position in self.control_point.preset_locations.missile_sites:
self.generate_missile_site() self.generate_missile_site_at(position)
def generate_missile_site(self) -> None:
position = self.location_finder.location_for(LocationType.MissileSite)
if position is not None:
return self.generate_missile_site_at(position)
def generate_missile_site_at(self, position: PointWithHeading) -> None: def generate_missile_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -776,17 +501,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
return return
def generate_required_coastal_sites(self) -> None:
for position in self.control_point.preset_locations.required_coastal_defenses:
self.generate_coastal_site_at(position)
def generate_coastal_sites(self) -> None: def generate_coastal_sites(self) -> None:
for i in range(self.faction.coastal_group_count): for position in self.control_point.preset_locations.coastal_defenses:
self.generate_coastal_site()
def generate_coastal_site(self) -> None:
position = self.location_finder.location_for(LocationType.Coastal)
if position is not None:
self.generate_coastal_site_at(position) self.generate_coastal_site_at(position)
def generate_coastal_site_at(self, position: PointWithHeading) -> None: def generate_coastal_site_at(self, position: PointWithHeading) -> None:
@ -807,46 +523,39 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return return
def generate_strike_targets(self) -> None: def generate_strike_targets(self) -> None:
"""Generates the strike targets that are required by the campaign."""
building_set = list(set(self.faction.building_set) - {"oil"}) building_set = list(set(self.faction.building_set) - {"oil"})
if not building_set: if not building_set:
logging.error("Faction has no buildings defined") logging.error("Faction has no buildings defined")
return return
for position in self.control_point.preset_locations.required_strike_locations: for position in self.control_point.preset_locations.strike_locations:
category = random.choice(building_set) category = random.choice(building_set)
self.generate_strike_target_at(category, position) self.generate_strike_target_at(category, position)
def generate_offshore_strike_targets(self) -> None: def generate_offshore_strike_targets(self) -> None:
"""Generates the offshore strike targets that are required by the campaign."""
if "oil" not in self.faction.building_set: if "oil" not in self.faction.building_set:
logging.error("Faction does not support offshore strike targets") logging.error("Faction does not support offshore strike targets")
return return
for ( for position in self.control_point.preset_locations.offshore_strike_locations:
position
) in self.control_point.preset_locations.required_offshore_strike_locations:
self.generate_strike_target_at("oil", position) self.generate_strike_target_at("oil", position)
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator): class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate(self) -> bool: def generate(self) -> bool:
self.generate_fob() self.generate_fob()
FobDefenseGenerator(self.game, self.control_point).generate()
self.generate_armor_groups() self.generate_armor_groups()
self.generate_factories() self.generate_factories()
self.generate_ammunition_depots() self.generate_ammunition_depots()
self.generate_required_aa() self.generate_aa()
self.generate_required_ewr() self.generate_ewrs()
self.generate_scenery_sites() self.generate_scenery_sites()
self.generate_strike_targets() self.generate_strike_targets()
self.generate_offshore_strike_targets() self.generate_offshore_strike_targets()
if self.faction.missiles: if self.faction.missiles:
self.generate_missile_sites() self.generate_missile_sites()
self.generate_required_missile_sites()
if self.faction.coastal_defenses: if self.faction.coastal_defenses:
self.generate_coastal_sites() self.generate_coastal_sites()
self.generate_required_coastal_sites()
return True return True

View File

@ -75,10 +75,16 @@ VERSION = _build_version_string()
#: * SPAAA_ZSU_23_4_Shilka_Gun_Dish, #: * SPAAA_ZSU_23_4_Shilka_Gun_Dish,
#: #:
#: Version 5.0 #: Version 5.0
#: * Ammunition Depots objective locations are now predetermined using the "Ammunition Depot" #: * Ammunition Depots objective locations are now predetermined using the "Ammunition
#: Warehouse object, and through trigger zone based scenery objects. # Depot" Warehouse object, and through trigger zone based scenery objects.
#: * The number of alive Ammunition Depot objective buildings connected to a control point #: * The number of alive Ammunition Depot objective buildings connected to a control
#: directly influences how many ground units can be supported on the front line. #: point directly influences how many ground units can be supported on the front
#: * The number of supported ground units at any control point is artificially capped at 50, #: line.
#: even if the number of alive Ammunition Depot objectives can support more. #: * The number of supported ground units at any control point is artificially
CAMPAIGN_FORMAT_VERSION = (5, 0) #: capped at 50, even if the number of alive Ammunition Depot objectives can
#: support more.
#:
#: Version 6.0
#: * Random objective generation no is longer supported. Fixed objective locations were
#: added in 4.1.
CAMPAIGN_FORMAT_VERSION = (6, 0)

View File

@ -7,5 +7,5 @@
"description": "<p>You have managed to establish a foothold at Khasab. Continue pushing south.</p>", "description": "<p>You have managed to establish a foothold at Khasab. Continue pushing south.</p>",
"miz": "battle_of_abu_dhabi.miz", "miz": "battle_of_abu_dhabi.miz",
"performance": 2, "performance": 2,
"version": "5.0" "version": "6.0"
} }

View File

@ -5,7 +5,7 @@
"recommended_player_faction": "USA 2005", "recommended_player_faction": "USA 2005",
"recommended_enemy_faction": "Insurgents (Hard)", "recommended_enemy_faction": "Insurgents (Hard)",
"description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>", "description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>",
"version": "5.0", "version": "6.0",
"miz": "inherent_resolve.miz", "miz": "inherent_resolve.miz",
"performance": 2 "performance": 2
} }