diff --git a/game/pretense/pretenseaircraftgenerator.py b/game/pretense/pretenseaircraftgenerator.py index f6c79c8b..334a8871 100644 --- a/game/pretense/pretenseaircraftgenerator.py +++ b/game/pretense/pretenseaircraftgenerator.py @@ -36,7 +36,6 @@ from game.theater.controlpoint import ( from game.unitmap import UnitMap from game.missiongenerator.aircraft.aircraftpainter import AircraftPainter from game.missiongenerator.aircraft.flightdata import FlightData -from game.missiongenerator.aircraft.flightgroupspawner import FlightGroupSpawner from game.data.weapons import WeaponType if TYPE_CHECKING: @@ -194,8 +193,10 @@ class PretenseAircraftGenerator: def create_and_configure_flight( self, flight: Flight, country: Country, dynamic_runways: Dict[str, RunwayData] ) -> FlyingGroup[Any]: + from game.pretense.pretenseflightgroupspawner import PretenseFlightGroupSpawner + """Creates and configures the flight group in the mission.""" - group = FlightGroupSpawner( + group = PretenseFlightGroupSpawner( flight, country, self.mission, diff --git a/game/pretense/pretenseflightgroupspawner.py b/game/pretense/pretenseflightgroupspawner.py index f3b19e87..908edcc0 100644 --- a/game/pretense/pretenseflightgroupspawner.py +++ b/game/pretense/pretenseflightgroupspawner.py @@ -1,53 +1,40 @@ import logging -import random -from typing import Any, Union, Tuple, Optional +import re +from typing import Any, Tuple from dcs import Mission from dcs.country import Country from dcs.mapping import Vector2, Point -from dcs.mission import StartType as DcsStartType -from dcs.planes import F_14A, Su_33 -from dcs.point import PointAction -from dcs.ships import KUZNECOW from dcs.terrain import NoParkingSlotError from dcs.unitgroup import ( FlyingGroup, ShipGroup, StaticGroup, - HelicopterGroup, - PlaneGroup, ) from game.ato import Flight -from game.ato.flightstate import InFlight from game.ato.starttype import StartType -from game.ato.traveltime import GroundSpeed +from game.missiongenerator.aircraft.flightgroupspawner import FlightGroupSpawner from game.missiongenerator.missiondata import MissionData -from game.naming import namegen -from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, OffMapSpawn +from game.naming import NameGenerator +from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint from game.utils import feet, meters -WARM_START_HELI_ALT = meters(500) -WARM_START_ALTITUDE = meters(3000) -# In-flight spawns are MSL for the first waypoint (this can maybe be changed to AGL, but -# AGL waypoints have different piloting behavior, so we need to check whether that's -# safe to do first), so spawn them high enough that they're unlikely to be near (or -# under) the ground, or any nearby obstacles. The highest airfield in DCS is Kerman in -# PG at 5700ft. This could still be too low if there are tall obstacles near the -# airfield, but the lowest we can push this the better to avoid spawning helicopters -# well above the altitude for WP1. -MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL = feet(6000) -MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL = feet(500) - -STACK_SEPARATION = feet(200) - -RTB_ALTITUDE = meters(800) -RTB_DISTANCE = 5000 -HELI_ALT = 500 +class PretenseNameGenerator(NameGenerator): + @classmethod + def next_pretense_aircraft_name(cls, cp: ControlPoint, flight: Flight) -> str: + cls.aircraft_number += 1 + cp_name_trimmed = "".join([i for i in cp.name.lower() if i.isalnum()]) + return "{}-{}-{}".format( + cp_name_trimmed, str(flight.flight_type).lower(), cls.aircraft_number + ) -class FlightGroupSpawner: +namegen = PretenseNameGenerator + + +class PretenseFlightGroupSpawner(FlightGroupSpawner): def __init__( self, flight: Flight, @@ -58,6 +45,16 @@ class FlightGroupSpawner: ground_spawns: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], mission_data: MissionData, ) -> None: + super().__init__( + flight, + country, + mission, + helipads, + ground_spawns_roadbase, + ground_spawns, + mission_data, + ) + self.flight = flight self.country = country self.mission = mission @@ -66,69 +63,11 @@ class FlightGroupSpawner: self.ground_spawns = ground_spawns self.mission_data = mission_data - def create_flight_group(self) -> FlyingGroup[Any]: - """Creates the group for the flight and adds it to the mission. - - Each flight is spawned according to its FlightState at the time of mission - generation. Aircraft that are WaitingForStart will be set up based on their - StartType with a delay. Note that delays are actually created during waypoint - generation. - - Aircraft that are *not* WaitingForStart will be spawned in their current state. - We cannot spawn aircraft mid-taxi, so when the simulated state is near the end - of a long taxi period the aircraft will be spawned in their parking spot. This - could lead to problems but that's what loiter points are for. The other pre- - flight states have the same problem but are much shorter and more easily covered - by the loiter time. Player flights that are spawned near the end of their cold - start have the biggest problem but players are able to cut corners to make up - for lost time. - - Aircraft that are already in the air will be spawned at their estimated - location, speed, and altitude based on their flight plan. - """ - if ( - self.flight.state.is_waiting_for_start - or self.flight.state.spawn_type is not StartType.IN_FLIGHT - ): - grp = self.generate_flight_at_departure() - self.flight.group_id = grp.id - return grp - grp = self.generate_mid_mission() - self.flight.group_id = grp.id - return grp - - def create_idle_aircraft(self) -> Optional[FlyingGroup[Any]]: - group = None - if ( - self.flight.is_helo - or self.flight.is_lha - and isinstance(self.flight.squadron.location, Fob) - ): - group = self._generate_at_cp_helipad( - name=namegen.next_aircraft_name(self.country, self.flight), - cp=self.flight.squadron.location, - ) - elif isinstance(self.flight.squadron.location, Fob): - group = self._generate_at_cp_ground_spawn( - name=namegen.next_aircraft_name(self.country, self.flight), - cp=self.flight.squadron.location, - ) - elif isinstance(self.flight.squadron.location, Airfield): - group = self._generate_at_airfield( - name=namegen.next_aircraft_name(self.country, self.flight), - airfield=self.flight.squadron.location, - ) - if group: - group.uncontrolled = True - return group - - @property - def start_type(self) -> StartType: - return self.flight.state.spawn_type - def generate_flight_at_departure(self) -> FlyingGroup[Any]: - name = namegen.next_aircraft_name(self.country, self.flight) cp = self.flight.departure + name = namegen.next_pretense_aircraft_name(cp, self.flight) + + print(name) try: if self.start_type is StartType.IN_FLIGHT: group = self._generate_over_departure(name, cp) @@ -198,236 +137,3 @@ class FlightGroupSpawner: self.flight.start_type = StartType.IN_FLIGHT group = self._generate_over_departure(name, cp) return group - - def generate_mid_mission(self) -> FlyingGroup[Any]: - assert isinstance(self.flight.state, InFlight) - name = namegen.next_aircraft_name(self.country, self.flight) - speed = self.flight.state.estimate_speed() - pos = self.flight.state.estimate_position() - pos += Vector2(random.randint(100, 1000), random.randint(100, 1000)) - alt, alt_type = self.flight.state.estimate_altitude() - cp = self.flight.squadron.location.id - - if cp not in self.mission_data.cp_stack: - self.mission_data.cp_stack[cp] = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL - - # We don't know where the ground is, so just make sure that any aircraft - # spawning at an MSL altitude is spawned at some minimum altitude. - # https://github.com/dcs-liberation/dcs_liberation/issues/1941 - if alt_type == "BARO" and alt < MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL: - alt = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL - - # Set a minimum AGL value for 'alt' if needed, - # otherwise planes might crash in trees and stuff. - if alt_type == "RADIO" and alt < self.mission_data.cp_stack[cp]: - alt = self.mission_data.cp_stack[cp] - self.mission_data.cp_stack[cp] += STACK_SEPARATION - - group = self.mission.flight_group( - country=self.country, - name=name, - aircraft_type=self.flight.unit_type.dcs_unit_type, - airport=None, - position=pos, - altitude=alt.meters, - speed=speed.kph, - maintask=None, - group_size=self.flight.count, - ) - - group.points[0].alt_type = alt_type - return group - - def _generate_at_airfield(self, name: str, airfield: Airfield) -> FlyingGroup[Any]: - # TODO: Delayed runway starts should be converted to air starts for multiplayer. - # Runway starts do not work with late activated aircraft in multiplayer. Instead - # of spawning on the runway the aircraft will spawn on the taxiway, potentially - # somewhere that they don't fit anyway. We should either upgrade these to air - # starts or (less likely) downgrade to warm starts to avoid the issue when the - # player is generating the mission for multiplayer (which would need a new - # option). - self.flight.unit_type.dcs_unit_type.load_payloads() - return self.mission.flight_group_from_airport( - country=self.country, - name=name, - aircraft_type=self.flight.unit_type.dcs_unit_type, - airport=airfield.airport, - maintask=None, - start_type=self._start_type_at_airfield(airfield), - group_size=self.flight.count, - parking_slots=None, - ) - - def _generate_over_departure( - self, name: str, origin: ControlPoint - ) -> FlyingGroup[Any]: - at = origin.position - - alt_type = "RADIO" - if isinstance(origin, OffMapSpawn): - alt = self.flight.flight_plan.waypoints[0].alt - alt_type = self.flight.flight_plan.waypoints[0].alt_type - elif self.flight.unit_type.helicopter: - alt = WARM_START_HELI_ALT - else: - if origin.id not in self.mission_data.cp_stack: - min_alt = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL - self.mission_data.cp_stack[origin.id] = min_alt - alt = self.mission_data.cp_stack[origin.id] - self.mission_data.cp_stack[origin.id] += STACK_SEPARATION - - speed = GroundSpeed.for_flight(self.flight, alt) - pos = at + Vector2(random.randint(100, 1000), random.randint(100, 1000)) - - group = self.mission.flight_group( - country=self.country, - name=name, - aircraft_type=self.flight.unit_type.dcs_unit_type, - airport=None, - position=pos, - altitude=alt.meters, - speed=speed.kph, - maintask=None, - group_size=self.flight.count, - ) - - group.points[0].alt_type = alt_type - return group - - def _generate_at_group( - self, name: str, at: Union[ShipGroup, StaticGroup] - ) -> FlyingGroup[Any]: - return self.mission.flight_group_from_unit( - country=self.country, - name=name, - aircraft_type=self.flight.unit_type.dcs_unit_type, - pad_group=at, - maintask=None, - start_type=self._start_type_at_group(at), - group_size=self.flight.count, - ) - - def _generate_at_cp_helipad( - self, name: str, cp: ControlPoint - ) -> Optional[FlyingGroup[Any]]: - try: - helipad = self.helipads[cp].pop() - except IndexError as ex: - logging.warning("Not enough helipads available at " + str(ex)) - if isinstance(cp, Airfield): - return self._generate_at_airfield(name, cp) - else: - return None - # raise RuntimeError(f"Not enough helipads available at {cp}") from ex - - group = self._generate_at_group(name, helipad) - - # Note : A bit dirty, need better support in pydcs - group.points[0].action = PointAction.FromGroundArea - group.points[0].type = "TakeOffGround" - group.units[0].heading = helipad.units[0].heading - if self.start_type is not StartType.COLD: - group.points[0].action = PointAction.FromGroundAreaHot - group.points[0].type = "TakeOffGroundHot" - - wpt = group.waypoint("LANDING") - if wpt: - hpad = self.helipads[self.flight.arrival].pop(0) - wpt.helipad_id = hpad.units[0].id - wpt.link_unit = hpad.units[0].id - self.helipads[self.flight.arrival].append(hpad) - - for i in range(self.flight.count - 1): - try: - helipad = self.helipads[cp].pop() - terrain = cp.coalition.game.theater.terrain - group.units[1 + i].position = Point( - helipad.x, helipad.y, terrain=terrain - ) - group.units[1 + i].heading = helipad.units[0].heading - except IndexError as ex: - logging.warning("Not enough helipads available at " + str(ex)) - if isinstance(cp, Airfield): - return self._generate_at_airfield(name, cp) - else: - if isinstance(group, HelicopterGroup): - self.country.helicopter_group.remove(group) - elif isinstance(group, PlaneGroup): - self.country.plane_group.remove(group) - return None - return group - - def _generate_at_cp_ground_spawn( - self, name: str, cp: ControlPoint - ) -> Optional[FlyingGroup[Any]]: - try: - if len(self.ground_spawns_roadbase[cp]) > 0: - ground_spawn = self.ground_spawns_roadbase[cp].pop() - else: - ground_spawn = self.ground_spawns[cp].pop() - except IndexError as ex: - logging.warning("Not enough STOL slots available at " + str(ex)) - return None - # raise RuntimeError(f"Not enough STOL slots available at {cp}") from ex - - group = self._generate_at_group(name, ground_spawn[0]) - - # Note : A bit dirty, need better support in pydcs - group.points[0].action = PointAction.FromGroundArea - group.points[0].type = "TakeOffGround" - group.units[0].heading = ground_spawn[0].units[0].heading - - try: - cp.coalition.game.scenery_clear_zones - except AttributeError: - cp.coalition.game.scenery_clear_zones = [] - cp.coalition.game.scenery_clear_zones.append(ground_spawn[1]) - - for i in range(self.flight.count - 1): - try: - terrain = cp.coalition.game.theater.terrain - if len(self.ground_spawns_roadbase[cp]) > 0: - ground_spawn = self.ground_spawns_roadbase[cp].pop() - else: - ground_spawn = self.ground_spawns[cp].pop() - group.units[1 + i].position = Point( - ground_spawn[0].x, ground_spawn[0].y, terrain=terrain - ) - group.units[1 + i].heading = ground_spawn[0].units[0].heading - except IndexError as ex: - raise RuntimeError(f"Not enough STOL slots available at {cp}") from ex - return group - - def dcs_start_type(self) -> DcsStartType: - if self.start_type is StartType.RUNWAY: - return DcsStartType.Runway - elif self.start_type is StartType.COLD: - return DcsStartType.Cold - elif self.start_type is StartType.WARM: - return DcsStartType.Warm - raise ValueError(f"There is no pydcs StartType matching {self.start_type}") - - def _start_type_at_airfield( - self, - airfield: Airfield, - ) -> DcsStartType: - return self.dcs_start_type() - - def _start_type_at_group( - self, - at: Union[ShipGroup, StaticGroup], - ) -> DcsStartType: - group_units = at.units - # Setting Su-33s starting from the non-supercarrier Kuznetsov to take off from - # runway to work around a DCS AI issue preventing Su-33s from taking off when - # set to "Takeoff from ramp" (#1352) - # Also setting the F-14A AI variant to start from cats since they are reported - # to have severe pathfinding problems when doing ramp starts (#1927) - if self.flight.unit_type.dcs_unit_type == F_14A or ( - self.flight.unit_type.dcs_unit_type == Su_33 - and group_units[0] is not None - and group_units[0].type == KUZNECOW.id - ): - return DcsStartType.Runway - else: - return self.dcs_start_type() diff --git a/game/pretense/pretensetriggergenerator.py b/game/pretense/pretensetriggergenerator.py index 11932f87..e5ea05e9 100644 --- a/game/pretense/pretensetriggergenerator.py +++ b/game/pretense/pretensetriggergenerator.py @@ -50,6 +50,9 @@ TRIGGER_RADIUS_MEDIUM = 100000 TRIGGER_RADIUS_LARGE = 150000 TRIGGER_RADIUS_ALL_MAP = 3000000 TRIGGER_RADIUS_CLEAR_SCENERY = 1000 +TRIGGER_RADIUS_PRETENSE_TGO = 500 +TRIGGER_RADIUS_PRETENSE_SUPPLY = 500 +TRIGGER_RADIUS_PRETENSE_HELI = 1000 class Silence(Option): @@ -156,7 +159,7 @@ class PretenseTriggerGenerator: for cp in self.game.theater.controlpoints: if isinstance(cp, self.capture_zone_types) and not cp.is_fleet: - zone_color = {1: 0.0, 2: 0.0, 3: 0.0, 4: 0.149} + zone_color = {1: 0.0, 2: 0.0, 3: 0.0, 4: 0.15} trigger_zone = self.mission.triggers.add_triggerzone( cp.position, radius=TRIGGER_RADIUS_CAPTURE, @@ -164,6 +167,45 @@ class PretenseTriggerGenerator: name=cp.name, color=zone_color, ) + cp_name_trimmed = "".join([i for i in cp.name.lower() if i.isalnum()]) + tgo_num = 0 + for tgo in cp.ground_objects: + tgo_num += 1 + zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15} + trigger_zone = self.mission.triggers.add_triggerzone( + tgo.position, + radius=TRIGGER_RADIUS_PRETENSE_TGO, + hidden=False, + name=f"{cp_name_trimmed}-{tgo_num}", + color=zone_color, + ) + for helipad in cp.helipads + cp.helipads_invisible + cp.helipads_quad: + zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15} + trigger_zone = self.mission.triggers.add_triggerzone( + position=helipad, + radius=TRIGGER_RADIUS_PRETENSE_HELI, + hidden=False, + name=f"{cp_name_trimmed}-hsp", + color=zone_color, + ) + break + for supply_route in cp.convoy_routes.values(): + tgo_num += 1 + zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15} + origin_position = supply_route[0] + next_position = supply_route[1] + convoy_heading = origin_position.heading_between_point(next_position) + supply_position = origin_position.point_from_heading( + convoy_heading, 300 + ) + trigger_zone = self.mission.triggers.add_triggerzone( + supply_position, + radius=TRIGGER_RADIUS_PRETENSE_TGO, + hidden=False, + name=f"{cp_name_trimmed}-sp", + color=zone_color, + ) + break def generate(self) -> None: player_coalition = "blue"