From 07cc85f6fa9ebc11da5787f1c9de8f19a47d5586 Mon Sep 17 00:00:00 2001 From: MetalStormGhost <89945461+MetalStormGhost@users.noreply.github.com> Date: Mon, 1 Jul 2024 23:26:50 +0300 Subject: [PATCH] Large aircraft ground spawns (#237) * Large aircraft ground spawns Added the ability to add separate ground spawns for C-130 and other large aircraft to campaigns. Implemented on @holyorangejuice 's request. Large aircraft ground spawns are added to the campaign by placing a C-130 on the ramp, just like an A-10 or AJS37 previously. Note: use the stock DCS C-130, so the campaign miz can be safely opened without the C-130 mod (or any other mod) installed. Not the C-130J player-flyable transport, not the KC-130J tanker included in the UH-60L mod etc. Large planes (wingspan more than 40 meters, such as the C-130): - First try spawning on large ground spawns - Then try the regular airfield ramp spawns Below 40 meter wingspan aircraft: - First try spawning on regular or roadbase ground spawns - Then try the regular airfield ramp spawns - Then, if both of the above fail, use the large ground spawns * Specify explicit black version 23.9.1 to fix lint error. * Update lint.yml --------- Co-authored-by: Raffson --- changelog.md | 1 + game/campaignloader/mizcampaignloader.py | 13 +- .../aircraft/aircraftgenerator.py | 4 + .../aircraft/flightgroupspawner.py | 139 +++++++++++++----- game/missiongenerator/missiongenerator.py | 1 + game/missiongenerator/tgogenerator.py | 130 +++++++++++++++- game/theater/controlpoint.py | 25 +++- 7 files changed, 267 insertions(+), 46 deletions(-) diff --git a/changelog.md b/changelog.md index 1c696026..df7f38cc 100644 --- a/changelog.md +++ b/changelog.md @@ -80,6 +80,7 @@ * **[Mission Generator]** Set F-14's IP waypoint according to the flight-plan's ingress point * **[Mission Generator]** Automatically de-spawn aircraft when arrival/divert is an off-map spawn * **[Options]** Option to de-spawn AI flights in the air if their start-type was manually set to In-Flight +* **[Campaign Design]** Ability to add separate ground spawns for C-130 and other large aircraft to campaigns. * **[Config]** Preference setting to use custom Liberation payloads instead of prioritizing Retribution's default * **[Config]** Preference setting to configure the server-port on which Retribution's back-end will run * **[Options]** Made AI jettisoning empty fuel tanks optional (disabled by default) diff --git a/game/campaignloader/mizcampaignloader.py b/game/campaignloader/mizcampaignloader.py index 1dcc028c..76737a33 100644 --- a/game/campaignloader/mizcampaignloader.py +++ b/game/campaignloader/mizcampaignloader.py @@ -9,7 +9,7 @@ from uuid import UUID from dcs import Mission from dcs.countries import CombinedJointTaskForcesBlue, CombinedJointTaskForcesRed from dcs.country import Country -from dcs.planes import F_15C, A_10A, AJS37 +from dcs.planes import F_15C, A_10A, AJS37, C_130 from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa from dcs.statics import Fortification, Warehouse from dcs.terrain import Airport @@ -43,6 +43,7 @@ class MizCampaignLoader: OFF_MAP_UNIT_TYPE = F_15C.id GROUND_SPAWN_UNIT_TYPE = A_10A.id GROUND_SPAWN_ROADBASE_UNIT_TYPE = AJS37.id + GROUND_SPAWN_LARGE_UNIT_TYPE = C_130.id CV_UNIT_TYPE = Stennis.id LHA_UNIT_TYPE = LHA_Tarawa.id @@ -237,6 +238,12 @@ class MizCampaignLoader: if group.units[0].type == self.GROUND_SPAWN_ROADBASE_UNIT_TYPE: yield group + @property + def ground_spawns_large(self) -> Iterator[PlaneGroup]: + for group in itertools.chain(self.blue.plane_group, self.red.plane_group): + if group.units[0].type == self.GROUND_SPAWN_LARGE_UNIT_TYPE: + yield group + @property def ground_spawns(self) -> Iterator[PlaneGroup]: for group in itertools.chain(self.blue.plane_group, self.red.plane_group): @@ -536,6 +543,10 @@ class MizCampaignLoader: closest, distance = self.objective_info(plane_group) self._add_ground_spawn(closest.ground_spawns_roadbase, plane_group) + for plane_group in self.ground_spawns_large: + closest, distance = self.objective_info(plane_group) + self._add_ground_spawn(closest.ground_spawns_large, plane_group) + for plane_group in self.ground_spawns: closest, distance = self.objective_info(plane_group) self._add_ground_spawn(closest.ground_spawns, plane_group) diff --git a/game/missiongenerator/aircraft/aircraftgenerator.py b/game/missiongenerator/aircraft/aircraftgenerator.py index 63179163..0370b9bb 100644 --- a/game/missiongenerator/aircraft/aircraftgenerator.py +++ b/game/missiongenerator/aircraft/aircraftgenerator.py @@ -56,6 +56,7 @@ class AircraftGenerator: mission_data: MissionData, helipads: dict[ControlPoint, list[StaticGroup]], ground_spawns_roadbase: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], + ground_spawns_large: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], ground_spawns: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], ) -> None: self.mission = mission @@ -69,6 +70,7 @@ class AircraftGenerator: self.mission_data = mission_data self.helipads = helipads self.ground_spawns_roadbase = ground_spawns_roadbase + self.ground_spawns_large = ground_spawns_large self.ground_spawns = ground_spawns self.ewrj_package_dict: Dict[int, List[FlyingGroup[Any]]] = {} @@ -208,6 +210,7 @@ class AircraftGenerator: self.mission, self.helipads, self.ground_spawns_roadbase, + self.ground_spawns_large, self.ground_spawns, self.mission_data, ).create_idle_aircraft() @@ -239,6 +242,7 @@ class AircraftGenerator: self.mission, self.helipads, self.ground_spawns_roadbase, + self.ground_spawns_large, self.ground_spawns, self.mission_data, ).create_flight_group() diff --git a/game/missiongenerator/aircraft/flightgroupspawner.py b/game/missiongenerator/aircraft/flightgroupspawner.py index 40f00cd5..dfd50248 100644 --- a/game/missiongenerator/aircraft/flightgroupspawner.py +++ b/game/missiongenerator/aircraft/flightgroupspawner.py @@ -66,6 +66,7 @@ class FlightGroupSpawner: mission: Mission, helipads: dict[ControlPoint, list[StaticGroup]], ground_spawns_roadbase: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], + ground_spawns_large: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], ground_spawns: dict[ControlPoint, list[Tuple[StaticGroup, Point]]], mission_data: MissionData, ) -> None: @@ -74,6 +75,7 @@ class FlightGroupSpawner: self.mission = mission self.helipads = helipads self.ground_spawns_roadbase = ground_spawns_roadbase + self.ground_spawns_large = ground_spawns_large self.ground_spawns = ground_spawns self.mission_data = mission_data @@ -177,6 +179,8 @@ class FlightGroupSpawner: raise RuntimeError( f"Cannot spawn fixed-wing aircraft at {cp} because of insufficient ground spawn slots." ) + is_large = self.flight.unit_type.dcs_unit_type.width > 40 + pilot_count = len(self.flight.roster.members) if ( not is_heli @@ -192,10 +196,18 @@ class FlightGroupSpawner: pad_group = self._generate_at_cp_helipad(name, cp) if pad_group is not None: return pad_group + if cp.has_ground_spawns and self.flight.client_count > 0 and is_large: + pad_group = self._generate_at_cp_ground_spawn(name, cp, is_large) + if pad_group is not None: + return pad_group if cp.has_ground_spawns and (self.flight.client_count > 0 or is_heli): pad_group = self._generate_at_cp_ground_spawn(name, cp) if pad_group is not None: return pad_group + else: + pad_group = self._generate_at_cp_ground_spawn(name, cp, True) + if pad_group is not None: + return pad_group return self._generate_over_departure(name, cp) elif isinstance(cp, Airfield): is_heli = self.flight.squadron.aircraft.helicopter @@ -203,6 +215,35 @@ class FlightGroupSpawner: pad_group = self._generate_at_cp_helipad(name, cp) if pad_group is not None: return pad_group + # Large planes (wingspan more than 40 meters, looking at you, C-130) + # First try spawning on large ground spawns + # Then try the regular airfield ramp spawns + is_large = self.flight.unit_type.dcs_unit_type.width > 40 + if ( + cp.has_ground_spawns + and is_large + and len(self.ground_spawns_large[cp]) >= self.flight.count + and (self.flight.client_count > 0) + ): + pad_group = self._generate_at_cp_ground_spawn(name, cp, is_large) + if pad_group is not None: + return pad_group + # Below 40 meter wingspan aircraft + # First try spawning on regular or roadbase ground spawns + # Then try the regular airfield ramp spawns + # Then, if both of the above fail, use the large ground spawns + if ( + cp.has_ground_spawns + and len(self.ground_spawns[cp]) + + len(self.ground_spawns_roadbase[cp]) + + len(self.ground_spawns_large[cp]) + >= self.flight.count + and (self.flight.client_count > 0 or is_heli) + ): + pad_group = self._generate_at_cp_ground_spawn(name, cp) + if pad_group is not None: + return pad_group + if ( cp.has_ground_spawns and len(self.ground_spawns[cp]) @@ -213,33 +254,45 @@ class FlightGroupSpawner: pad_group = self._generate_at_cp_ground_spawn(name, cp) if pad_group is not None: return pad_group - - # TODO: get rid of the nevatim hack once fixed in DCS... - if self._check_nevatim_hack(cp): - slots = [ - slot - for slot in cp.dcs_airport.free_parking_slots( - self.flight.squadron.aircraft.dcs_unit_type - ) - if slot.slot_name in [str(n) for n in range(55, 66)] - ] - return self._generate_at_airfield(name, cp, slots) - elif self._check_ramon_airbase_hack(cp): - # TODO: get rid of the ramon airbase hack once fixed in DCS... - slots = [ - slot - for slot in cp.dcs_airport.free_parking_slots( - self.flight.squadron.aircraft.dcs_unit_type - ) - if slot.slot_name - not in [ - str(n) - for n in [1, 2, 3, 4, 5, 6, 13, 14, 15, 16, 17, 18, 61] + try: + # TODO: get rid of the nevatim hack once fixed in DCS... + if self._check_nevatim_hack(cp): + slots = [ + slot + for slot in cp.dcs_airport.free_parking_slots( + self.flight.squadron.aircraft.dcs_unit_type + ) + if slot.slot_name in [str(n) for n in range(55, 66)] ] - ] - return self._generate_at_airfield(name, cp, slots) - else: - return self._generate_at_airfield(name, cp) + return self._generate_at_airfield(name, cp, slots) + elif self._check_ramon_airbase_hack(cp): + # TODO: get rid of the ramon airbase hack once fixed in DCS... + slots = [ + slot + for slot in cp.dcs_airport.free_parking_slots( + self.flight.squadron.aircraft.dcs_unit_type + ) + if slot.slot_name + not in [ + str(n) + for n in [1, 2, 3, 4, 5, 6, 13, 14, 15, 16, 17, 18, 61] + ] + ] + return self._generate_at_airfield(name, cp, slots) + else: + return self._generate_at_airfield(name, cp) + except NoParkingSlotError: + if ( + cp.has_ground_spawns + and len(self.ground_spawns_large[cp]) >= self.flight.count + and (self.flight.client_count > 0 or is_heli) + ): + pad_group = self._generate_at_cp_ground_spawn(name, cp, True) + if pad_group is not None: + return pad_group + else: + raise NoParkingSlotError + return self._generate_at_airfield(name, cp) else: raise NotImplementedError( f"Aircraft spawn behavior not implemented for {cp} ({cp.__class__})" @@ -439,22 +492,26 @@ class FlightGroupSpawner: return group def _generate_at_cp_ground_spawn( - self, name: str, cp: ControlPoint + self, name: str, cp: ControlPoint, is_large: bool = False ) -> Optional[FlyingGroup[Any]]: is_airbase = False is_roadbase = False try: - if len(self.ground_spawns_roadbase[cp]) > 0: - ground_spawn = self.ground_spawns_roadbase[cp].pop() - is_roadbase = True + if is_large: + if len(self.ground_spawns_large[cp]) > 0: + ground_spawn = self.ground_spawns_large[cp].pop() + is_airbase = True else: - ground_spawn = self.ground_spawns[cp].pop() - is_airbase = True + if len(self.ground_spawns_roadbase[cp]) > 0: + ground_spawn = self.ground_spawns_roadbase[cp].pop() + is_roadbase = True + if len(self.ground_spawns[cp]) > 0: + ground_spawn = self.ground_spawns[cp].pop() + is_airbase = True except IndexError as ex: - logging.warning("Not enough STOL slots available at " + str(ex)) + logging.warning("Not enough ground spawn 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]) @@ -490,16 +547,22 @@ class FlightGroupSpawner: 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() + if is_large: + if len(self.ground_spawns_large[cp]) > 0: + ground_spawn = self.ground_spawns_large[cp].pop() else: - ground_spawn = self.ground_spawns[cp].pop() + 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 + raise RuntimeError( + f"Not enough ground spawn slots available at {cp}" + ) from ex return group def dcs_start_type(self) -> DcsStartType: diff --git a/game/missiongenerator/missiongenerator.py b/game/missiongenerator/missiongenerator.py index 2a7eebd8..ce13a695 100644 --- a/game/missiongenerator/missiongenerator.py +++ b/game/missiongenerator/missiongenerator.py @@ -249,6 +249,7 @@ class MissionGenerator: mission_data=self.mission_data, helipads=tgo_generator.helipads, ground_spawns_roadbase=tgo_generator.ground_spawns_roadbase, + ground_spawns_large=tgo_generator.ground_spawns_large, ground_spawns=tgo_generator.ground_spawns, ) diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py index 215aaaa4..d8f2c550 100644 --- a/game/missiongenerator/tgogenerator.py +++ b/game/missiongenerator/tgogenerator.py @@ -1007,6 +1007,123 @@ class GroundSpawnRoadbaseGenerator: self.ground_spawns_roadbase = [] +class GroundSpawnLargeGenerator: + """ + Generates STOL aircraft starting positions for given control point + """ + + def __init__( + self, + mission: Mission, + cp: ControlPoint, + game: Game, + radio_registry: RadioRegistry, + tacan_registry: TacanRegistry, + ): + self.m = mission + self.cp = cp + self.game = game + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry + self.ground_spawns_large: list[Tuple[StaticGroup, Point]] = [] + + def create_ground_spawn_large( + self, i: int, vtol_pad: Tuple[PointWithHeading, Point] + ) -> None: + # Note: FARPs are generated as neutral object in order not to interfere with + # capture triggers + neutral_country = self.m.country(self.game.neutral_country.name) + country = self.m.country( + self.game.coalition_for(self.cp.captured).faction.country.name + ) + terrain = self.cp.coalition.game.theater.terrain + + name = f"{self.cp.name} large ground spawn {i}" + logging.info("Generating Large Ground Spawn static : " + name) + + pad = InvisibleFARP(unit_id=self.m.next_unit_id(), name=name, terrain=terrain) + + pad.position = Point(vtol_pad[0].x, vtol_pad[0].y, terrain=terrain) + pad.heading = vtol_pad[0].heading.degrees + sg = unitgroup.StaticGroup(self.m.next_group_id(), name) + sg.add_unit(pad) + sp = StaticPoint(pad.position) + sg.add_point(sp) + neutral_country.add_static_group(sg) + + self.ground_spawns_large.append((sg, vtol_pad[1])) + + # tanker_type: Type[VehicleType] + # ammo_truck_type: Type[VehicleType] + + tanker_type, ammo_truck_type, power_truck_type = farp_truck_types_for_country( + country.id + ) + + # Generate a FARP Ammo and Fuel stack for each pad + if self.game.settings.ground_start_trucks: + self.m.vehicle_group( + country=country, + name=(name + "_fuel"), + _type=tanker_type, + position=pad.position.point_from_heading( + vtol_pad[0].heading.degrees - 175, 45 + ), + group_size=1, + heading=pad.heading + 45, + move_formation=PointAction.OffRoad, + ) + self.m.vehicle_group( + country=country, + name=(name + "_ammo"), + _type=ammo_truck_type, + position=pad.position.point_from_heading( + vtol_pad[0].heading.degrees - 185, 45 + ), + group_size=1, + heading=pad.heading + 45, + move_formation=PointAction.OffRoad, + ) + else: + self.m.static_group( + country=country, + name=(name + "_fuel"), + _type=Fortification.FARP_Fuel_Depot, + position=pad.position.point_from_heading( + vtol_pad[0].heading.degrees - 180, 55 + ), + heading=pad.heading, + ) + self.m.static_group( + country=country, + name=(name + "_ammo"), + _type=Fortification.FARP_Ammo_Dump_Coating, + position=pad.position.point_from_heading( + vtol_pad[0].heading.degrees - 180, 45 + ), + heading=pad.heading + 270, + ) + if self.game.settings.ground_start_ground_power_trucks: + self.m.vehicle_group( + country=country, + name=(name + "_power"), + _type=power_truck_type, + position=pad.position.point_from_heading( + vtol_pad[0].heading.degrees - 185, 45 + ), + group_size=1, + heading=pad.heading + 45, + move_formation=PointAction.OffRoad, + ) + + def generate(self) -> None: + try: + for i, vtol_pad in enumerate(self.cp.ground_spawns_large): + self.create_ground_spawn_large(i, vtol_pad) + except AttributeError: + self.ground_spawns_large = [] + + class GroundSpawnGenerator: """ Generates STOL aircraft starting positions for given control point @@ -1153,6 +1270,9 @@ class TgoGenerator: self.ground_spawns_roadbase: dict[ ControlPoint, list[Tuple[StaticGroup, Point]] ] = defaultdict(list) + self.ground_spawns_large: dict[ + ControlPoint, list[Tuple[StaticGroup, Point]] + ] = defaultdict(list) self.ground_spawns: dict[ ControlPoint, list[Tuple[StaticGroup, Point]] ] = defaultdict(list) @@ -1179,7 +1299,15 @@ class TgoGenerator: ] = ground_spawn_roadbase_gen.ground_spawns_roadbase random.shuffle(self.ground_spawns_roadbase[cp]) - # Generate STOL pads + # Generate Large Ground Spawn slots + ground_large_spawn_gen = GroundSpawnLargeGenerator( + self.m, cp, self.game, self.radio_registry, self.tacan_registry + ) + ground_large_spawn_gen.generate() + self.ground_spawns_large[cp] = ground_large_spawn_gen.ground_spawns_large + random.shuffle(self.ground_spawns_large[cp]) + + # Generate Ground Spawn slots ground_spawn_gen = GroundSpawnGenerator( self.m, cp, self.game, self.radio_registry, self.tacan_registry ) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index c58e7174..c5fe8801 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -390,6 +390,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): self.helipads_quad: List[PointWithHeading] = [] self.helipads_invisible: List[PointWithHeading] = [] self.ground_spawns_roadbase: List[Tuple[PointWithHeading, Point]] = [] + self.ground_spawns_large: List[Tuple[PointWithHeading, Point]] = [] self.ground_spawns: List[Tuple[PointWithHeading, Point]] = [] self._coalition: Optional[Coalition] = None @@ -611,7 +612,12 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): """ Returns true if cp can operate STOL aircraft """ - return len(self.ground_spawns_roadbase) + len(self.ground_spawns) > 0 + return ( + len(self.ground_spawns_roadbase) + + len(self.ground_spawns_large) + + len(self.ground_spawns) + > 0 + ) def can_recruit_ground_units(self, game: Game) -> bool: """Returns True if this control point is capable of recruiting ground units.""" @@ -1283,6 +1289,7 @@ class Airfield(ControlPoint, CTLD): if parking_type.include_fixed_wing_stol: parking_slots += len(self.ground_spawns) parking_slots += len(self.ground_spawns_roadbase) + parking_slots += len(self.ground_spawns_large) if parking_type.include_fixed_wing: parking_slots += len(self.airport.parking_slots) return parking_slots @@ -1672,13 +1679,19 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD): + len(self.helipads_invisible) ) - try: - if parking_type.include_fixed_wing_stol: + if parking_type.include_fixed_wing_stol: + try: parking_slots += len(self.ground_spawns) + except AttributeError: + self.ground_spawns_roadbase = [] + try: parking_slots += len(self.ground_spawns_roadbase) - except AttributeError: - self.ground_spawns_roadbase = [] - self.ground_spawns = [] + except AttributeError: + self.ground_spawns_large = [] + try: + parking_slots += len(self.ground_spawns_large) + except AttributeError: + self.ground_spawns = [] return parking_slots def can_operate(self, aircraft: AircraftType) -> bool: