dcs_liberation/game/theater/start_generator.py
2023-04-26 00:36:34 -07:00

533 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
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()