Add support for required variants of all TGOs.

Adds required variants of:

* SHORADS
* Armor groups
* Buildings
* Oil rigs
* Coastal defenses
* Missile sites
* Ships

This is prep work for removing random generation.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1076
This commit is contained in:
Dan Albert 2021-05-17 22:24:31 -07:00
parent 8076206a90
commit 739406614d
5 changed files with 250 additions and 28 deletions

View File

@ -10,8 +10,10 @@ Saves from 2.5 are not compatible with 3.0.
* **[Campaign AI]** Every 30 minutes the AI will plan a CAP, so players can customize their mission better.
* **[UI]** Added new web based map UI. This is mostly functional but many of the old display options are a WIP. Revert to the old map with --old-map.
* **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present.
* **[UI]** DCS loadouts are now selectable in the loadout setup menu.
* **[Modding]** Campaigns now choose locations for factories to spawn.
* **[Modding]** Campaigns now use map structures as strike targets.
* **[Modding]** Campaigns may now set *any* objective type to be a required spawn rather than random chance.
* **[Modding]** Can now install custom factions to <DCS saved games>/Liberation/Factions instead of the Liberation install directory.
* **[Performance Settings]** Added a settings to lower the number of smoke effects generated on frontlines. Lowered default settings for frontline smoke generators, so less smoke should be generated by default.
* **[Configuration]** Liberation preferences (DCS install and save game location) are now saved to `%LOCALAPPDATA%/DCSLiberation` to prevent needing to reconfigure each new install.
@ -29,7 +31,6 @@ Saves from 2.5 are not compatible with 3.0.
* **[UI]** Engagement ranges are now displayed by default.
* **[UI]** Engagement range display generalized to work for all patrolling flight plans (BARCAP, TARCAP, and CAS).
* **[UI]** DCS loadouts are now selectable in the loadout setup menu.
* **[Flight Planner]** Front lines no longer project threat zones to avoid pushing BARCAPs back so much. TARCAPs will be forcibly planned but strike packages will not route around front lines even if it is reasonable to do so.
## Fixes

View File

@ -40,8 +40,6 @@ from dcs.unitgroup import (
VehicleGroup,
)
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from dcs.triggers import Triggers
from dcs.triggers import TriggerZone
from ..scenery_group import SceneryGroup
from pyproj import CRS, Transformer
@ -110,10 +108,21 @@ class MizCampaignLoader:
AirDefence.SAM_SA_3_S_125_Goa_LN.id,
}
REQUIRED_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,
}
REQUIRED_EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
REQUIRED_STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
BASE_DEFENSE_RADIUS = nautical_miles(2)
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
@ -196,6 +205,12 @@ class MizCampaignLoader:
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:
if group.units[0].type == self.SHIP_UNIT_TYPE:
yield group
@property
def ewrs(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
@ -220,18 +235,36 @@ class MizCampaignLoader:
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:
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
yield group
@property
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:
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
yield group
@property
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:
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
yield group
@property
def required_long_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
@ -244,12 +277,24 @@ class MizCampaignLoader:
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
yield group
@property
def required_short_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_SHORT_RANGE_SAM_UNIT_TYPES:
yield group
@property
def required_ewrs(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_EWR_UNIT_TYPE:
yield group
@property
def armor_groups(self) -> Iterator[VehicleGroup]:
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
if group.units[0].type in self.ARMOR_GROUP_UNIT_TYPE:
yield group
@property
def helipads(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
@ -262,6 +307,12 @@ class MizCampaignLoader:
if group.units[0].type in self.FACTORY_UNIT_TYPE:
yield group
@property
def required_strike_targets(self) -> Iterator[StaticGroup]:
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:
yield group
@property
def scenery(self) -> List[SceneryGroup]:
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
@ -405,24 +456,48 @@ class MizCampaignLoader:
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:
closest, distance = self.objective_info(group)
closest.preset_locations.ships.append(
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:
closest, distance = self.objective_info(group)
closest.preset_locations.missile_sites.append(
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:
closest, distance = self.objective_info(group)
closest.preset_locations.coastal_defenses.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_coastal_defenses:
closest, distance = self.objective_info(group)
closest.preset_locations.required_coastal_defenses.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_long_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.required_long_range_sams.append(
@ -435,12 +510,24 @@ class MizCampaignLoader:
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_short_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.required_short_range_sams.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)
)
for group in self.armor_groups:
closest, distance = self.objective_info(group)
closest.preset_locations.armor_groups.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.helipads:
closest, distance = self.objective_info(group)
closest.helipads.append(
@ -453,6 +540,12 @@ class MizCampaignLoader:
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_strike_targets:
closest, distance = self.objective_info(group)
closest.preset_locations.required_strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.scenery:
closest, distance = self.objective_info(group)
closest.preset_locations.scenery.append(group)

View File

@ -94,24 +94,47 @@ class PresetLocations:
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
ships: List[PointWithHeading] = field(default_factory=list)
#: Locations used by non-carrier ships that will be spawned unless the faction has
#: 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)
#: 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.
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.
offshore_strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by offshore strike objectives that will always be spawned.
required_offshore_strike_locations: List[PointWithHeading] = field(
default_factory=list
)
#: Locations used by missile sites like scuds and V-2s.
missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations used by missile sites like scuds and V-2s that are always generated if
#: the faction is capable.
required_missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations of long range SAMs which should always be spawned.
required_long_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of medium range SAMs which should always be spawned.
required_medium_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of short range SAMs which should always be spawned.
required_short_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of EWRs which should always be spawned.
required_ewrs: List[PointWithHeading] = field(default_factory=list)
@ -121,6 +144,9 @@ class PresetLocations:
#: Locations of factories for producing ground units. These will always be spawned.
factories: List[PointWithHeading] = field(default_factory=list)
#: Locations of stationary armor groups. These will always be spawned.
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."""

View File

@ -188,10 +188,7 @@ class ControlPointGroundObjectGenerator:
def generate(self) -> bool:
self.control_point.connected_objectives = []
if self.faction.navy_generators:
# Even airbases can generate navies if they are close enough to the
# water. This is not controlled by the control point definition, but
# rather by whether or not the generator can find a valid position
# for the ship.
# Even airbases can generate navies if they are close enough to the water.
self.generate_navy()
return True
@ -205,18 +202,24 @@ class ControlPointGroundObjectGenerator:
if not self.control_point.captured and skip_enemy_navy:
return
self.generate_required_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)
def generate_ship(self) -> None:
point = self.location_finder.location_for(LocationType.Ship)
if point is None:
return
if point is not None:
self.generate_ship_at(point)
def generate_ship_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = ShipGroundObject(
namegen.random_objective_name(), group_id, point, self.control_point
namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_ship_group(self.game, g, self.faction_name)
@ -453,21 +456,26 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
BaseDefenseGenerator(self.game, self.control_point).generate()
self.generate_ground_points()
if self.faction.missiles:
self.generate_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
return True
def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point."""
self.generate_armor_groups()
skip_sams = self.generate_required_aa()
skip_ewrs = self.generate_required_ewr()
self.generate_scenery_sites()
self.generate_strike_targets()
self.generate_offshore_strike_targets()
self.generate_factories()
if self.faction.missiles:
self.generate_missile_sites()
self.generate_required_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
self.generate_required_coastal_sites()
if self.control_point.is_global:
return
@ -492,6 +500,32 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
else:
self.generate_ground_point()
def generate_armor_groups(self) -> None:
for position in self.control_point.preset_locations.armor_groups:
self.generate_armor_at(position)
def generate_armor_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = VehicleGroupGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=False,
)
group = generate_armor_group(self.faction_name, self.game, g)
if group is None:
logging.error(
"Could not generate armor group for %s at %s",
g.name,
self.control_point,
)
return
g.groups = [group]
self.control_point.connected_objectives.append(g)
def generate_required_aa(self) -> int:
"""Generates the AA sites that are required by the campaign.
@ -516,8 +550,15 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
{AirDefenseRange.Short},
],
)
return len(presets.required_long_range_sams) + len(
presets.required_medium_range_sams
for position in presets.required_short_range_sams:
self.generate_aa_at(
position,
ranges=[{AirDefenseRange.Short}],
)
return (
len(presets.required_long_range_sams)
+ len(presets.required_medium_range_sams)
+ len(presets.required_short_range_sams)
)
def generate_required_ewr(self) -> int:
@ -538,9 +579,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
logging.exception("Faction has no buildings defined")
return
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
if category == "oil":
location_type = LocationType.OffshoreStrikeTarget
else:
@ -551,6 +589,13 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
if point is None:
return
self.generate_strike_target_at(category, point)
def generate_strike_target_at(self, category: str, position: Point) -> None:
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
object_id = 0
group_id = self.game.next_group_id()
@ -564,7 +609,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
category,
group_id,
object_id,
point + template_point,
position + template_point,
unit["heading"],
self.control_point,
unit["type"],
@ -633,7 +678,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return
self.generate_ewr_at(position)
def generate_ewr_at(self, position: Point) -> None:
def generate_ewr_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = EwrGroundObject(
@ -688,15 +733,20 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
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:
for i in range(self.faction.missiles_group_count):
self.generate_missile_site()
def generate_missile_site(self) -> None:
position = self.location_finder.location_for(LocationType.MissileSite)
if position is None:
return
if position is not None:
return self.generate_missile_site_at(position)
def generate_missile_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = MissileSiteGroundObject(
@ -709,15 +759,20 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
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:
for i in range(self.faction.coastal_group_count):
self.generate_coastal_site()
def generate_coastal_site(self) -> None:
position = self.location_finder.location_for(LocationType.Coastal)
if position is None:
return
if position is not None:
self.generate_coastal_site_at(position)
def generate_coastal_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = CoastalSiteGroundObject(
@ -734,13 +789,47 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
return
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"})
if not building_set:
logging.error("Faction has no buildings defined")
return
for position in self.control_point.preset_locations.required_strike_locations:
category = random.choice(building_set)
self.generate_strike_target_at(category, position)
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:
logging.error("Faction does not support offshore strike targets")
return
for (
position
) in self.control_point.preset_locations.required_offshore_strike_locations:
self.generate_strike_target_at("oil", position)
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate(self) -> bool:
self.generate_fob()
FobDefenseGenerator(self.game, self.control_point).generate()
self.generate_armor_groups()
self.generate_factories()
self.generate_required_aa()
self.generate_required_ewr()
self.generate_scenery_sites()
self.generate_strike_targets()
self.generate_offshore_strike_targets()
if self.faction.missiles:
self.generate_missile_sites()
self.generate_required_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
self.generate_required_coastal_sites()
return True
def generate_fob(self) -> None:

View File

@ -53,4 +53,17 @@ VERSION = _build_version_string()
#: by a blue circular TriggerZone, campaign creation will fail. Blue circular
#: TriggerZones must also have their first property's value field define the type of
#: objective (a valid value for a building TGO category, from `game.db.PRICES`).
CAMPAIGN_FORMAT_VERSION = (4, 0)
#:
#: Version 4.1
#: * All objective types may now be set as required generation (similar to the required
#: IADS generation). This includes:
#: * SHORADS
#: * Armor groups
#: * Strike targets
#: * Offshore strike targets
#: * Ships
#: * Missile sites
#: * Coastal defenses
#:
#: See the unit lists in MizCampaignLoader in conflicttheater.py for unit types.
CAMPAIGN_FORMAT_VERSION = (4, 1)