mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
F/A-18E and F/A-18F are added to their factions, Growler is currently in its respective factions but was released in 2009, this will be changed if needed.
534 lines
19 KiB
Python
534 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import random
|
|
from dataclasses import dataclass, fields
|
|
from datetime import datetime, time
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
import dcs.statics
|
|
import yaml
|
|
|
|
from game import Game
|
|
from game.factions.faction import Faction
|
|
from game.naming import namegen
|
|
from game.scenery_group import SceneryGroup
|
|
from game.theater import PointWithHeading, PresetLocation
|
|
from game.theater.theatergroundobject import (
|
|
BuildingGroundObject,
|
|
IadsBuildingGroundObject,
|
|
)
|
|
from game.utils import Heading, escape_string_for_lua
|
|
from game.version import VERSION
|
|
from . import (
|
|
ConflictTheater,
|
|
ControlPoint,
|
|
ControlPointType,
|
|
Fob,
|
|
OffMapSpawn,
|
|
)
|
|
from .theatergroup import IadsGroundGroup, IadsRole, SceneryUnit, TheaterGroup
|
|
from ..armedforces.armedforces import ArmedForces
|
|
from ..armedforces.forcegroup import ForceGroup
|
|
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
|
from ..data.groups import GroupTask
|
|
from ..persistence.paths import liberation_user_dir
|
|
from ..plugins import LuaPluginManager
|
|
from ..profiling import logged_duration
|
|
from ..settings import Settings
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GeneratorSettings:
|
|
start_date: datetime
|
|
start_time: time | None
|
|
player_budget: int
|
|
enemy_budget: int
|
|
inverted: bool
|
|
advanced_iads: bool
|
|
no_carrier: bool
|
|
no_lha: bool
|
|
no_player_navy: bool
|
|
no_enemy_navy: bool
|
|
|
|
|
|
@dataclass
|
|
class ModSettings:
|
|
a4_skyhawk: bool = False
|
|
f22_raptor: bool = False
|
|
f104_starfighter: bool = False
|
|
f4_phantom: bool = False
|
|
hercules: bool = False
|
|
uh_60l: bool = False
|
|
jas39_gripen: bool = False
|
|
su57_felon: bool = False
|
|
frenchpack: bool = False
|
|
high_digit_sams: bool = False
|
|
ov10a_bronco: bool = False
|
|
superhornet: bool = False
|
|
|
|
def save_player_settings(self) -> None:
|
|
"""Saves the player's global settings to the user directory."""
|
|
settings: dict[str, dict[str, bool]] = {}
|
|
for field in fields(self):
|
|
settings[field.name] = self.__dict__[field.name]
|
|
|
|
with self._player_settings_file.open("w", encoding="utf-8") as settings_file:
|
|
yaml.dump(settings, settings_file, sort_keys=False, explicit_start=True)
|
|
|
|
def merge_player_settings(self) -> None:
|
|
"""Updates with the player's global settings."""
|
|
settings_path = self._player_settings_file
|
|
if not settings_path.exists():
|
|
return
|
|
with settings_path.open(encoding="utf-8") as settings_file:
|
|
data = yaml.safe_load(settings_file)
|
|
|
|
for mod_name, enabled in data.items():
|
|
if mod_name not in self.__dict__:
|
|
logging.warning(
|
|
"Unexpected mod key found in %s: %s. Ignoring.",
|
|
settings_path,
|
|
mod_name,
|
|
)
|
|
continue
|
|
self.__dict__[mod_name] = enabled
|
|
|
|
@property
|
|
def _player_settings_file(self) -> Path:
|
|
"""Returns the path to the player's global settings file."""
|
|
return liberation_user_dir() / "mods.yaml"
|
|
|
|
|
|
class GameGenerator:
|
|
def __init__(
|
|
self,
|
|
player: Faction,
|
|
enemy: Faction,
|
|
theater: ConflictTheater,
|
|
air_wing_config: CampaignAirWingConfig,
|
|
settings: Settings,
|
|
generator_settings: GeneratorSettings,
|
|
mod_settings: ModSettings,
|
|
lua_plugin_manager: LuaPluginManager,
|
|
) -> None:
|
|
self.player = player
|
|
self.enemy = enemy
|
|
self.theater = theater
|
|
self.air_wing_config = air_wing_config
|
|
self.settings = settings
|
|
self.generator_settings = generator_settings
|
|
self.player.apply_mod_settings(mod_settings)
|
|
self.enemy.apply_mod_settings(mod_settings)
|
|
self.lua_plugin_manager = lua_plugin_manager
|
|
|
|
def generate(self) -> Game:
|
|
with logged_duration("TGO population"):
|
|
# Reset name generator
|
|
namegen.reset()
|
|
self.prepare_theater()
|
|
game = Game(
|
|
player_faction=self.player,
|
|
enemy_faction=self.enemy,
|
|
theater=self.theater,
|
|
air_wing_config=self.air_wing_config,
|
|
start_date=self.generator_settings.start_date,
|
|
start_time=self.generator_settings.start_time,
|
|
settings=self.settings,
|
|
lua_plugin_manager=self.lua_plugin_manager,
|
|
player_budget=self.generator_settings.player_budget,
|
|
enemy_budget=self.generator_settings.enemy_budget,
|
|
)
|
|
|
|
GroundObjectGenerator(game, self.generator_settings).generate()
|
|
game.settings.version = VERSION
|
|
return game
|
|
|
|
def should_remove_carrier(self, player: bool) -> bool:
|
|
faction = self.player if player else self.enemy
|
|
return self.generator_settings.no_carrier or not faction.carrier_names
|
|
|
|
def should_remove_lha(self, player: bool) -> bool:
|
|
faction = self.player if player else self.enemy
|
|
return self.generator_settings.no_lha or not faction.helicopter_carrier_names
|
|
|
|
def prepare_theater(self) -> None:
|
|
to_remove: List[ControlPoint] = []
|
|
|
|
# Remove carrier and lha, invert situation if needed
|
|
for cp in self.theater.controlpoints:
|
|
if self.generator_settings.inverted:
|
|
cp.starts_blue = cp.captured_invert
|
|
|
|
if cp.is_carrier and self.should_remove_carrier(cp.starts_blue):
|
|
to_remove.append(cp)
|
|
elif cp.is_lha and self.should_remove_lha(cp.starts_blue):
|
|
to_remove.append(cp)
|
|
|
|
# do remove
|
|
for cp in to_remove:
|
|
self.theater.controlpoints.remove(cp)
|
|
|
|
|
|
class ControlPointGroundObjectGenerator:
|
|
def __init__(
|
|
self,
|
|
game: Game,
|
|
generator_settings: GeneratorSettings,
|
|
control_point: ControlPoint,
|
|
) -> None:
|
|
self.game = game
|
|
self.generator_settings = generator_settings
|
|
self.control_point = control_point
|
|
|
|
@property
|
|
def faction_name(self) -> str:
|
|
return self.faction.name
|
|
|
|
@property
|
|
def faction(self) -> Faction:
|
|
return self.game.coalition_for(self.control_point.captured).faction
|
|
|
|
@property
|
|
def armed_forces(self) -> ArmedForces:
|
|
return self.game.coalition_for(self.control_point.captured).armed_forces
|
|
|
|
def generate(self) -> bool:
|
|
self.control_point.connected_objectives = []
|
|
self.generate_navy()
|
|
return True
|
|
|
|
def generate_ground_object_from_group(
|
|
self, unit_group: ForceGroup, location: PresetLocation
|
|
) -> None:
|
|
ground_object = unit_group.generate(
|
|
namegen.random_objective_name(),
|
|
location,
|
|
self.control_point,
|
|
self.game,
|
|
)
|
|
self.control_point.connected_objectives.append(ground_object)
|
|
|
|
def generate_navy(self) -> None:
|
|
skip_player_navy = self.generator_settings.no_player_navy
|
|
if self.control_point.captured and skip_player_navy:
|
|
return
|
|
skip_enemy_navy = self.generator_settings.no_enemy_navy
|
|
if not self.control_point.captured and skip_enemy_navy:
|
|
return
|
|
for position in self.control_point.preset_locations.ships:
|
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.NAVY)
|
|
if not unit_group:
|
|
logging.warning(f"{self.faction_name} has no ForceGroup for Navy")
|
|
return
|
|
self.generate_ground_object_from_group(unit_group, position)
|
|
|
|
|
|
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|
def generate(self) -> bool:
|
|
return True
|
|
|
|
|
|
class GenericCarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|
def update_carrier_name(self, carrier_name: str) -> None:
|
|
# Set Control Point name
|
|
self.control_point.name = carrier_name
|
|
|
|
# Set UnitName. First unit of the TGO is always the carrier
|
|
carrier = next(self.control_point.ground_objects[-1].units)
|
|
carrier.name = carrier_name
|
|
|
|
|
|
class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
|
|
def generate(self) -> bool:
|
|
if not super().generate():
|
|
return False
|
|
|
|
carrier_names = self.faction.carrier_names
|
|
if not carrier_names:
|
|
logging.info(
|
|
f"Skipping generation of {self.control_point.name} because "
|
|
f"{self.faction_name} has no carriers"
|
|
)
|
|
return False
|
|
|
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.AIRCRAFT_CARRIER)
|
|
if not unit_group:
|
|
logging.error(f"{self.faction_name} has no access to AircraftCarrier")
|
|
return False
|
|
self.generate_ground_object_from_group(
|
|
unit_group,
|
|
PresetLocation(
|
|
self.control_point.name,
|
|
self.control_point.position,
|
|
self.control_point.heading,
|
|
),
|
|
)
|
|
self.update_carrier_name(random.choice(carrier_names))
|
|
return True
|
|
|
|
|
|
class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
|
|
def generate(self) -> bool:
|
|
if not super().generate():
|
|
return False
|
|
|
|
lha_names = self.faction.helicopter_carrier_names
|
|
if not lha_names:
|
|
logging.info(
|
|
f"Skipping generation of {self.control_point.name} because "
|
|
f"{self.faction_name} has no LHAs"
|
|
)
|
|
return False
|
|
|
|
unit_group = self.armed_forces.random_group_for_task(
|
|
GroupTask.HELICOPTER_CARRIER
|
|
)
|
|
if not unit_group:
|
|
logging.error(f"{self.faction_name} has no access to HelicopterCarrier")
|
|
return False
|
|
self.generate_ground_object_from_group(
|
|
unit_group,
|
|
PresetLocation(
|
|
self.control_point.name,
|
|
self.control_point.position,
|
|
self.control_point.heading,
|
|
),
|
|
)
|
|
self.update_carrier_name(random.choice(lha_names))
|
|
return True
|
|
|
|
|
|
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|
def __init__(
|
|
self,
|
|
game: Game,
|
|
generator_settings: GeneratorSettings,
|
|
control_point: ControlPoint,
|
|
) -> None:
|
|
super().__init__(game, generator_settings, control_point)
|
|
|
|
def generate(self) -> bool:
|
|
if not super().generate():
|
|
return False
|
|
|
|
self.generate_ground_points()
|
|
return True
|
|
|
|
def generate_ground_points(self) -> None:
|
|
"""Generate ground objects and AA sites for the control point."""
|
|
self.generate_armor_groups()
|
|
self.generate_iads()
|
|
self.generate_scenery_sites()
|
|
self.generate_strike_targets()
|
|
self.generate_offshore_strike_targets()
|
|
self.generate_factories()
|
|
self.generate_ammunition_depots()
|
|
self.generate_missile_sites()
|
|
self.generate_coastal_sites()
|
|
|
|
def generate_armor_groups(self) -> None:
|
|
for position in self.control_point.preset_locations.armor_groups:
|
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.BASE_DEFENSE)
|
|
if not unit_group:
|
|
logging.error(f"{self.faction_name} has no ForceGroup for Armor")
|
|
return
|
|
self.generate_ground_object_from_group(unit_group, position)
|
|
|
|
def generate_aa(self) -> None:
|
|
presets = self.control_point.preset_locations
|
|
aa_tasking = [GroupTask.AAA]
|
|
for position in presets.aaa:
|
|
self.generate_aa_at(position, aa_tasking)
|
|
aa_tasking.insert(0, GroupTask.SHORAD)
|
|
for position in presets.short_range_sams:
|
|
self.generate_aa_at(position, aa_tasking)
|
|
aa_tasking.insert(0, GroupTask.MERAD)
|
|
for position in presets.medium_range_sams:
|
|
self.generate_aa_at(position, aa_tasking)
|
|
aa_tasking.insert(0, GroupTask.LORAD)
|
|
for position in presets.long_range_sams:
|
|
self.generate_aa_at(position, aa_tasking)
|
|
|
|
def generate_ewrs(self) -> None:
|
|
for position in self.control_point.preset_locations.ewrs:
|
|
unit_group = self.armed_forces.random_group_for_task(
|
|
GroupTask.EARLY_WARNING_RADAR
|
|
)
|
|
if not unit_group:
|
|
logging.error(f"{self.faction_name} has no ForceGroup for EWR")
|
|
return
|
|
self.generate_ground_object_from_group(unit_group, position)
|
|
|
|
def generate_building_at(
|
|
self,
|
|
group_task: GroupTask,
|
|
location: PresetLocation,
|
|
) -> None:
|
|
# GroupTask is the type of the building to be generated
|
|
unit_group = self.armed_forces.random_group_for_task(group_task)
|
|
if not unit_group:
|
|
raise RuntimeError(
|
|
f"{self.faction_name} has no access to Building {group_task.description}"
|
|
)
|
|
self.generate_ground_object_from_group(unit_group, location)
|
|
|
|
def generate_ammunition_depots(self) -> None:
|
|
for position in self.control_point.preset_locations.ammunition_depots:
|
|
self.generate_building_at(GroupTask.AMMO, position)
|
|
|
|
def generate_factories(self) -> None:
|
|
for position in self.control_point.preset_locations.factories:
|
|
self.generate_building_at(GroupTask.FACTORY, position)
|
|
|
|
def generate_aa_at(self, location: PresetLocation, tasks: list[GroupTask]) -> None:
|
|
for task in tasks:
|
|
unit_group = self.armed_forces.random_group_for_task(task)
|
|
if unit_group:
|
|
# Only take next (smaller) aa_range when no template available for the
|
|
# most requested range. Otherwise break the loop and continue
|
|
self.generate_ground_object_from_group(unit_group, location)
|
|
return
|
|
|
|
logging.error(
|
|
f"{self.faction_name} has no access to SAM {', '.join([task.description for task in tasks])}"
|
|
)
|
|
|
|
def generate_iads(self) -> None:
|
|
# AntiAir
|
|
self.generate_aa()
|
|
# EWR
|
|
self.generate_ewrs()
|
|
# IADS Buildings
|
|
for iads_element in self.control_point.preset_locations.iads_command_center:
|
|
self.generate_building_at(GroupTask.COMMAND_CENTER, iads_element)
|
|
for iads_element in self.control_point.preset_locations.iads_connection_node:
|
|
self.generate_building_at(GroupTask.COMMS, iads_element)
|
|
for iads_element in self.control_point.preset_locations.iads_power_source:
|
|
self.generate_building_at(GroupTask.POWER, iads_element)
|
|
|
|
def generate_scenery_sites(self) -> None:
|
|
presets = self.control_point.preset_locations
|
|
for scenery_group in presets.scenery:
|
|
self.generate_tgo_for_scenery(scenery_group)
|
|
|
|
def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None:
|
|
# Special Handling for scenery Objects based on trigger zones
|
|
iads_role = IadsRole.for_category(scenery.category)
|
|
tgo_type = (
|
|
IadsBuildingGroundObject if iads_role.participate else BuildingGroundObject
|
|
)
|
|
g = tgo_type(
|
|
namegen.random_objective_name(),
|
|
scenery.category,
|
|
PresetLocation(scenery.name, scenery.centroid),
|
|
self.control_point,
|
|
)
|
|
ground_group = TheaterGroup(
|
|
self.game.next_group_id(),
|
|
scenery.name,
|
|
PointWithHeading.from_point(scenery.centroid, Heading.from_degrees(0)),
|
|
[],
|
|
g,
|
|
)
|
|
if iads_role.participate:
|
|
ground_group = IadsGroundGroup.from_group(ground_group)
|
|
ground_group.iads_role = iads_role
|
|
|
|
g.groups.append(ground_group)
|
|
# Each nested trigger zone is a target/building/unit for an objective.
|
|
for zone in scenery.target_zones:
|
|
zone.name = escape_string_for_lua(zone.name)
|
|
scenery_unit = SceneryUnit(
|
|
zone.id,
|
|
zone.name,
|
|
dcs.statics.Fortification.White_Flag,
|
|
PointWithHeading.from_point(zone.position, Heading.from_degrees(0)),
|
|
g,
|
|
)
|
|
scenery_unit.zone = zone
|
|
ground_group.units.append(scenery_unit)
|
|
|
|
self.control_point.connected_objectives.append(g)
|
|
|
|
def generate_missile_sites(self) -> None:
|
|
for position in self.control_point.preset_locations.missile_sites:
|
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.MISSILE)
|
|
if not unit_group:
|
|
logging.warning(f"{self.faction_name} has no ForceGroup for Missile")
|
|
return
|
|
self.generate_ground_object_from_group(unit_group, position)
|
|
|
|
def generate_coastal_sites(self) -> None:
|
|
for position in self.control_point.preset_locations.coastal_defenses:
|
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.COASTAL)
|
|
if not unit_group:
|
|
logging.warning(f"{self.faction_name} has no ForceGroup for Coastal")
|
|
return
|
|
self.generate_ground_object_from_group(unit_group, position)
|
|
|
|
def generate_strike_targets(self) -> None:
|
|
for position in self.control_point.preset_locations.strike_locations:
|
|
self.generate_building_at(GroupTask.STRIKE_TARGET, position)
|
|
|
|
def generate_offshore_strike_targets(self) -> None:
|
|
for position in self.control_point.preset_locations.offshore_strike_locations:
|
|
self.generate_building_at(GroupTask.OIL, position)
|
|
|
|
|
|
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
|
def generate(self) -> bool:
|
|
if super(FobGroundObjectGenerator, self).generate():
|
|
self.generate_fob()
|
|
return True
|
|
return False
|
|
|
|
def generate_fob(self) -> None:
|
|
self.generate_building_at(
|
|
GroupTask.FOB,
|
|
PresetLocation(
|
|
self.control_point.name,
|
|
self.control_point.position,
|
|
self.control_point.heading,
|
|
),
|
|
)
|
|
|
|
|
|
class GroundObjectGenerator:
|
|
def __init__(self, game: Game, generator_settings: GeneratorSettings) -> None:
|
|
self.game = game
|
|
self.generator_settings = generator_settings
|
|
|
|
def generate(self) -> None:
|
|
# Copied so we can remove items from the original list without breaking
|
|
# the iterator.
|
|
control_points = list(self.game.theater.controlpoints)
|
|
for control_point in control_points:
|
|
if not self.generate_for_control_point(control_point):
|
|
self.game.theater.controlpoints.remove(control_point)
|
|
|
|
def generate_for_control_point(self, control_point: ControlPoint) -> bool:
|
|
generator: ControlPointGroundObjectGenerator
|
|
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
|
|
generator = CarrierGroundObjectGenerator(
|
|
self.game, self.generator_settings, control_point
|
|
)
|
|
elif control_point.cptype == ControlPointType.LHA_GROUP:
|
|
generator = LhaGroundObjectGenerator(
|
|
self.game, self.generator_settings, control_point
|
|
)
|
|
elif isinstance(control_point, OffMapSpawn):
|
|
generator = NoOpGroundObjectGenerator(
|
|
self.game, self.generator_settings, control_point
|
|
)
|
|
elif isinstance(control_point, Fob):
|
|
generator = FobGroundObjectGenerator(
|
|
self.game, self.generator_settings, control_point
|
|
)
|
|
else:
|
|
generator = AirbaseGroundObjectGenerator(
|
|
self.game, self.generator_settings, control_point
|
|
)
|
|
return generator.generate()
|