From 87b630f8d5ede02e7fcbd286b104c078b693d95b Mon Sep 17 00:00:00 2001 From: Raffson Date: Mon, 26 Feb 2024 00:00:27 +0100 Subject: [PATCH] Initial attempt --- game/campaignloader/campaign.py | 8 +++ game/campaignloader/campaigncarrierconfig.py | 42 ++++++++++++++++ game/factions/faction.py | 41 ++++++++++++++-- game/migrator.py | 2 - game/theater/start_generator.py | 51 +++++++++++++++++--- qt_ui/main.py | 1 + qt_ui/windows/newgame/QNewGameWizard.py | 1 + resources/campaigns/1968_Yankee_Station.yaml | 10 +++- resources/factions/usn_2005.json | 6 +-- resources/groups/Carrier_Strike_Group_8.yaml | 4 ++ resources/units/ships/CVN_71.yaml | 4 ++ resources/units/ships/CVN_72.yaml | 4 ++ resources/units/ships/CVN_73.yaml | 4 ++ resources/units/ships/CVN_75.yaml | 4 ++ resources/units/ships/CV_1143_5.yaml | 4 ++ tests/test_factions.py | 3 +- 16 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 game/campaignloader/campaigncarrierconfig.py create mode 100644 resources/units/ships/CVN_71.yaml create mode 100644 resources/units/ships/CVN_72.yaml create mode 100644 resources/units/ships/CVN_73.yaml create mode 100644 resources/units/ships/CVN_75.yaml create mode 100644 resources/units/ships/CV_1143_5.yaml diff --git a/game/campaignloader/campaign.py b/game/campaignloader/campaign.py index bf582d6c..1d78fc98 100644 --- a/game/campaignloader/campaign.py +++ b/game/campaignloader/campaign.py @@ -19,6 +19,7 @@ from game.theater.iadsnetwork.iadsnetwork import IadsNetwork from game.theater.theaterloader import TheaterLoader from game.version import CAMPAIGN_FORMAT_VERSION from .campaignairwingconfig import CampaignAirWingConfig +from .campaigncarrierconfig import CampaignCarrierConfig from .campaigngroundconfig import TgoConfig from .mizcampaignloader import MizCampaignLoader @@ -140,6 +141,13 @@ class Campaign: return CampaignAirWingConfig({}) return CampaignAirWingConfig.from_campaign_data(squadron_data, theater) + def load_carrier_config(self) -> CampaignCarrierConfig: + try: + carrier_data = self.data["carriers"] + except KeyError: + return CampaignCarrierConfig({}) + return CampaignCarrierConfig.from_campaign_data(carrier_data) + def load_ground_forces_config(self) -> TgoConfig: ground_forces = self.data.get("ground_forces", {}) if not ground_forces: diff --git a/game/campaignloader/campaigncarrierconfig.py b/game/campaignloader/campaigncarrierconfig.py new file mode 100644 index 00000000..8307572d --- /dev/null +++ b/game/campaignloader/campaigncarrierconfig.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import logging +from collections import defaultdict +from dataclasses import dataclass +from typing import Any, TYPE_CHECKING + +from game.dcs.shipunittype import ShipUnitType + +if TYPE_CHECKING: + pass + + +@dataclass(frozen=True) +class CarrierConfig: + preferred_name: str + preferred_type: ShipUnitType + + @classmethod + def from_data(cls, data: dict[str, Any]) -> CarrierConfig: + return CarrierConfig( + str(data["preferred_name"]), ShipUnitType.named(data["preferred_type"]) + ) + + +@dataclass(frozen=True) +class CampaignCarrierConfig: + by_original_name: dict[str, CarrierConfig] + + @classmethod + def from_campaign_data( + cls, data: dict[str, Any] + ) -> CampaignCarrierConfig: + by_original_name: dict[str, CarrierConfig] = defaultdict() + for original_name, carrier_config_data in data.items(): + try: + carrier_config = CarrierConfig.from_data(carrier_config_data) + by_original_name[original_name] = carrier_config + except KeyError: + logging.warning(f"Skipping invalid carrier config for '{original_name}'") + + return CampaignCarrierConfig(by_original_name) diff --git a/game/factions/faction.py b/game/factions/faction.py index 2dc4bd0e..22f000b6 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -2,6 +2,7 @@ from __future__ import annotations import itertools import logging +from collections import defaultdict from dataclasses import dataclass, field from functools import cached_property from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING, Set @@ -93,8 +94,8 @@ class Faction: # Required mods or asset packs requirements: Dict[str, str] = field(default_factory=dict) - # Possible carrier names - carrier_names: Set[str] = field(default_factory=set) + # Possible aircraft carrier units + carriers: Dict[ShipUnitType, Set[str]] = field(default_factory=dict) # Possible helicopter carrier names helicopter_carrier_names: Set[str] = field(default_factory=set) @@ -241,8 +242,29 @@ class Faction: faction.requirements = json.get("requirements", {}) - faction.carrier_names = json.get("carrier_names", []) - faction.helicopter_carrier_names = json.get("helicopter_carrier_names", []) + # First try to load the carriers in the new format which + # specifies different names for different carrier types + loaded_carriers = load_carriers(json) + + carriers: List[ShipUnitType] = [ + unit + for unit in faction.naval_units + if unit.unit_class + in [ + UnitClass.AIRCRAFT_CARRIER, + UnitClass.HELICOPTER_CARRIER, + ] + ] + carrier_names = json.get("carrier_names", []) + for c in carriers: + if c.variant_id not in loaded_carriers: + if c.unit_class == UnitClass.AIRCRAFT_CARRIER: + loaded_carriers[c] = carrier_names + elif c.unit_class == UnitClass.HELICOPTER_CARRIER: + loaded_carriers[c] = faction.helicopter_carrier_names + + faction.carriers = loaded_carriers + faction.naval_units.union(faction.carriers.keys()) faction.has_jtac = json.get("has_jtac", False) jtac_name = json.get("jtac_unit", None) @@ -592,3 +614,14 @@ def load_all_ships(data: list[str]) -> List[Type[ShipType]]: if item is not None: items.append(item) return items + + +def load_carriers(json: Dict[str, Any]) -> Dict[ShipUnitType, Set[str]]: + # Load carriers + items: Dict[ShipUnitType, Set[str]] = defaultdict(Set[str]) + carriers = json.get("carriers", {}) + for carrier_shiptype, shipnames in carriers.items(): + shiptype = ShipUnitType.named(carrier_shiptype) + if shiptype is not None: + items[shiptype] = shipnames + return items diff --git a/game/migrator.py b/game/migrator.py index 51715261..e1d05395 100644 --- a/game/migrator.py +++ b/game/migrator.py @@ -201,8 +201,6 @@ class Migrator: c.faction.air_defense_units = set(c.faction.air_defense_units) if isinstance(c.faction.missiles, list): c.faction.missiles = set(c.faction.missiles) - if isinstance(c.faction.carrier_names, list): - c.faction.carrier_names = set(c.faction.carrier_names) if isinstance(c.faction.helicopter_carrier_names, list): c.faction.helicopter_carrier_names = set( c.faction.helicopter_carrier_names diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index a1dac653..b162f316 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -12,7 +12,7 @@ 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 import PointWithHeading, PresetLocation, NavalControlPoint from game.theater.theatergroundobject import ( BuildingGroundObject, IadsBuildingGroundObject, @@ -30,8 +30,10 @@ from .theatergroup import IadsGroundGroup, IadsRole, SceneryUnit, TheaterGroup from ..armedforces.armedforces import ArmedForces from ..armedforces.forcegroup import ForceGroup from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig +from ..campaignloader.campaigncarrierconfig import CampaignCarrierConfig from ..campaignloader.campaigngroundconfig import TgoConfig from ..data.groups import GroupTask +from ..dcs.shipunittype import ShipUnitType from ..profiling import logged_duration from ..settings import Settings @@ -49,6 +51,7 @@ class GeneratorSettings: no_player_navy: bool no_enemy_navy: bool tgo_config: TgoConfig + carrier_config: CampaignCarrierConfig squadrons_start_full: bool @@ -125,7 +128,7 @@ class GameGenerator: 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 + return self.generator_settings.no_carrier or not faction.carriers def should_remove_lha(self, player: bool) -> bool: faction = self.player if player else self.enemy @@ -221,14 +224,49 @@ class GenericCarrierGroundObjectGenerator(ControlPointGroundObjectGenerator): carrier = next(self.control_point.ground_objects[-1].units) carrier.name = carrier_name + def apply_carrier_config(self) -> None: + assert isinstance(self.control_point, NavalControlPoint) + # If the campaign designer has specified a preferred name, use that + # Note that the preferred name needs to exist in the faction, so we + # don't end up with Kuznetsov carriers called CV-59 Forrestal + preferred_name = None + preferred_type = None + carrier_map = self.generator_settings.carrier_config.by_original_name + if ccfg := carrier_map.get(self.control_point.name): + preferred_name = ccfg.preferred_name + preferred_type = ccfg.preferred_type + carrier_unit = self.control_point.ground_objects[0].groups[0].units[0] + carrier_type = preferred_type if preferred_type else carrier_unit.unit_type + assert isinstance(carrier_type, ShipUnitType) + if preferred_type and self.faction.has_access_to_dcs_type(preferred_type.dcs_unit_type): + carrier_unit.type = carrier_type.dcs_unit_type + if ( + preferred_name + ): + self.control_point.name = preferred_name + else: + # Otherwise pick randomly from the names specified for that particular carrier type + carrier_names = self.faction.carriers.get(carrier_type) + if carrier_names: + self.control_point.name = random.choice(list(carrier_names)) + else: + self.control_point.name = carrier_type.display_name + # Prevents duplicate carrier or LHA names in campaigns with more that one of either. + for carrier_type_key in self.faction.carriers: + for carrier_name in self.faction.carriers[carrier_type_key]: + if carrier_name == self.control_point.name: + self.faction.carriers[carrier_type_key].remove( + self.control_point.name + ) + class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator): def generate(self) -> bool: if not super().generate(): return False - carrier_names = self.faction.carrier_names - if not carrier_names: + carriers = self.faction.carriers + if not carriers: logging.info( f"Skipping generation of {self.control_point.name} because " f"{self.faction_name} has no carriers" @@ -239,6 +277,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator): 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( @@ -248,7 +287,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator): ), GroupTask.AIRCRAFT_CARRIER, ) - self.update_carrier_name(random.choice(list(carrier_names))) + self.apply_carrier_config() return True @@ -280,7 +319,7 @@ class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator): ), GroupTask.HELICOPTER_CARRIER, ) - self.update_carrier_name(random.choice(list(lha_names))) + self.apply_carrier_config() return True diff --git a/qt_ui/main.py b/qt_ui/main.py index 1e4757ee..97908f9c 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -317,6 +317,7 @@ def create_game( no_player_navy=False, no_enemy_navy=False, tgo_config=campaign.load_ground_forces_config(), + carrier_config=campaign.load_carrier_config(), ), ModSettings( a4_skyhawk=False, diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 9cfcd033..59874aee 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -84,6 +84,7 @@ class NewGameWizard(QtWidgets.QWizard): no_player_navy=self.field("no_player_navy"), no_enemy_navy=self.field("no_enemy_navy"), tgo_config=campaign.load_ground_forces_config(), + carrier_config=campaign.load_carrier_config(), squadrons_start_full=self.field("squadrons_start_full"), ) mod_settings = ModSettings( diff --git a/resources/campaigns/1968_Yankee_Station.yaml b/resources/campaigns/1968_Yankee_Station.yaml index 2d0bd223..5251afaf 100644 --- a/resources/campaigns/1968_Yankee_Station.yaml +++ b/resources/campaigns/1968_Yankee_Station.yaml @@ -81,6 +81,14 @@ settings: #default="Full" +carriers: + Naval-1: + preferred_name: CVN-74 + preferred_type: CVN-74 John C. Stennis + Naval-2: + preferred_name: The Harry + preferred_type: CVN-75 Harry S. Truman + #Squadrons and order of battle settings squadrons: @@ -156,7 +164,7 @@ squadrons: aircraft: - CH-53E size: 2 - #CV-59 Forrestal + #CV-59 Forrestal Naval-2: - primary: AEW&C aircraft: diff --git a/resources/factions/usn_2005.json b/resources/factions/usn_2005.json index e0bbf390..ba8df39b 100644 --- a/resources/factions/usn_2005.json +++ b/resources/factions/usn_2005.json @@ -46,14 +46,14 @@ ], "preset_groups": [ "Hawk", - "Patriot" + "Patriot", + "Carrier Strike Group 8" ], "naval_units": [ "FFG Oliver Hazard Perry", "DDG Arleigh Burke IIa", "CG Ticonderoga", - "LHA-1 Tarawa", - "CVN-74 John C. Stennis" + "LHA-1 Tarawa" ], "missiles": [], "air_defense_units": [ diff --git a/resources/groups/Carrier_Strike_Group_8.yaml b/resources/groups/Carrier_Strike_Group_8.yaml index d91d6948..49b59087 100644 --- a/resources/groups/Carrier_Strike_Group_8.yaml +++ b/resources/groups/Carrier_Strike_Group_8.yaml @@ -2,7 +2,11 @@ name: Carrier Strike Group 8 tasks: - Navy units: + - CVN-71 Theodore Roosevelt + - CVN-72 Abraham Lincoln + - CVN-73 George Washington - CVN-74 John C. Stennis + - CVN-75 Harry S. Truman - DDG Arleigh Burke IIa - CG Ticonderoga layouts: diff --git a/resources/units/ships/CVN_71.yaml b/resources/units/ships/CVN_71.yaml new file mode 100644 index 00000000..fecc02b4 --- /dev/null +++ b/resources/units/ships/CVN_71.yaml @@ -0,0 +1,4 @@ +class: AircraftCarrier +price: 0 +variants: + CVN-71 Theodore Roosevelt: null \ No newline at end of file diff --git a/resources/units/ships/CVN_72.yaml b/resources/units/ships/CVN_72.yaml new file mode 100644 index 00000000..d454172e --- /dev/null +++ b/resources/units/ships/CVN_72.yaml @@ -0,0 +1,4 @@ +class: AircraftCarrier +price: 0 +variants: + CVN-72 Abraham Lincoln: null \ No newline at end of file diff --git a/resources/units/ships/CVN_73.yaml b/resources/units/ships/CVN_73.yaml new file mode 100644 index 00000000..5c1dfc35 --- /dev/null +++ b/resources/units/ships/CVN_73.yaml @@ -0,0 +1,4 @@ +class: AircraftCarrier +price: 0 +variants: + CVN-73 George Washington: null \ No newline at end of file diff --git a/resources/units/ships/CVN_75.yaml b/resources/units/ships/CVN_75.yaml new file mode 100644 index 00000000..2099d9dc --- /dev/null +++ b/resources/units/ships/CVN_75.yaml @@ -0,0 +1,4 @@ +class: AircraftCarrier +price: 0 +variants: + CVN-75 Harry S. Truman: null \ No newline at end of file diff --git a/resources/units/ships/CV_1143_5.yaml b/resources/units/ships/CV_1143_5.yaml new file mode 100644 index 00000000..6561eb5f --- /dev/null +++ b/resources/units/ships/CV_1143_5.yaml @@ -0,0 +1,4 @@ +class: AircraftCarrier +price: 0 +variants: + CV 1143.5 Admiral Kuznetsov(2017): null \ No newline at end of file diff --git a/tests/test_factions.py b/tests/test_factions.py index ec43dbe9..8eaecaa6 100644 --- a/tests/test_factions.py +++ b/tests/test_factions.py @@ -92,12 +92,13 @@ class TestFactionLoader(unittest.TestCase): self.assertIn(Infantry.Soldier_M249, faction.infantry_units) self.assertIn(Stennis.name, faction.naval_units) + self.assertIn(Stennis, faction.carriers.keys()) self.assertIn(LHA_Tarawa.name, faction.naval_units) self.assertIn("mod", faction.requirements.keys()) self.assertIn("Some mod is required", faction.requirements.values()) - self.assertEqual(4, len(faction.carrier_names)) + self.assertEqual(4, len(faction.carriers)) self.assertEqual(5, len(faction.helicopter_carrier_names)) @pytest.mark.skip(reason="Faction unit names in the json files are outdated")