Configurable carriers

This commit is contained in:
Raffson 2024-04-06 23:04:47 +02:00
parent e416e07366
commit d2fd7bbb4e
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
15 changed files with 173 additions and 24 deletions

View File

@ -8,6 +8,7 @@
* **[Squadrons]** Ability to define a livery-set for each squadron from which Retribution will randomly choose during mission generation * **[Squadrons]** Ability to define a livery-set for each squadron from which Retribution will randomly choose during mission generation
* **[Modding]** Updated support for F/A-18E/F/G mod version 2.2.5 * **[Modding]** Updated support for F/A-18E/F/G mod version 2.2.5
* **[Campaign Setup]** Allow adjustments to naval TGOs (except carriers) on turn 0 * **[Campaign Setup]** Allow adjustments to naval TGOs (except carriers) on turn 0
* **[Campaign Design]** Ability to configure specific carrier names & types in campaign's yaml file
## Fixes ## Fixes
* **[UI/UX]** A-10A flights can be edited again. * **[UI/UX]** A-10A flights can be edited again.

View File

@ -19,6 +19,7 @@ from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
from game.theater.theaterloader import TheaterLoader from game.theater.theaterloader import TheaterLoader
from game.version import CAMPAIGN_FORMAT_VERSION from game.version import CAMPAIGN_FORMAT_VERSION
from .campaignairwingconfig import CampaignAirWingConfig from .campaignairwingconfig import CampaignAirWingConfig
from .campaigncarrierconfig import CampaignCarrierConfig
from .campaigngroundconfig import TgoConfig from .campaigngroundconfig import TgoConfig
from .mizcampaignloader import MizCampaignLoader from .mizcampaignloader import MizCampaignLoader
from ..factions import FACTIONS, Faction from ..factions import FACTIONS, Faction
@ -164,6 +165,13 @@ class Campaign:
return CampaignAirWingConfig({}) return CampaignAirWingConfig({})
return CampaignAirWingConfig.from_campaign_data(squadron_data, theater) 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: def load_ground_forces_config(self) -> TgoConfig:
ground_forces = self.data.get("ground_forces", {}) ground_forces = self.data.get("ground_forces", {})
if not ground_forces: if not ground_forces:

View File

@ -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)

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import itertools import itertools
import logging import logging
from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property from functools import cached_property
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING, Set from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING, Set
@ -93,11 +94,8 @@ class Faction:
# Required mods or asset packs # Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict) requirements: Dict[str, str] = field(default_factory=dict)
# Possible carrier names # Possible carrier units mapped to names
carrier_names: Set[str] = field(default_factory=set) carriers: Dict[ShipUnitType, Set[str]] = field(default_factory=dict)
# Possible helicopter carrier names
helicopter_carrier_names: Set[str] = field(default_factory=set)
# Available Naval Units # Available Naval Units
naval_units: Set[ShipUnitType] = field(default_factory=set) naval_units: Set[ShipUnitType] = field(default_factory=set)
@ -241,8 +239,30 @@ class Faction:
faction.requirements = json.get("requirements", {}) faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", []) # First try to load the carriers in the new format which
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", []) # 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", [])
helicopter_carrier_names = json.get("helicopter_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] = helicopter_carrier_names
faction.carriers = loaded_carriers
faction.naval_units.union(faction.carriers.keys())
faction.has_jtac = json.get("has_jtac", False) faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None) jtac_name = json.get("jtac_unit", None)
@ -596,3 +616,14 @@ def load_all_ships(data: list[str]) -> List[Type[ShipType]]:
if item is not None: if item is not None:
items.append(item) items.append(item)
return items 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

View File

@ -208,12 +208,6 @@ class Migrator:
c.faction.air_defense_units = set(c.faction.air_defense_units) c.faction.air_defense_units = set(c.faction.air_defense_units)
if isinstance(c.faction.missiles, list): if isinstance(c.faction.missiles, list):
c.faction.missiles = set(c.faction.missiles) 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
)
if isinstance(c.faction.naval_units, list): if isinstance(c.faction.naval_units, list):
c.faction.naval_units = set(c.faction.naval_units) c.faction.naval_units = set(c.faction.naval_units)
if isinstance(c.faction.building_set, list): if isinstance(c.faction.building_set, list):

View File

@ -1395,6 +1395,11 @@ class NavalControlPoint(
L02, L02,
L52, L52,
L61, L61,
CV_1143_5,
CVN_71,
CVN_72,
CVN_73,
CVN_75,
]: ]:
return True return True
return False return False

View File

@ -7,12 +7,13 @@ from datetime import datetime, time
from typing import List, Optional from typing import List, Optional
import dcs.statics import dcs.statics
from dcs.countries import country_dict
from game import Game from game import Game
from game.factions.faction import Faction from game.factions.faction import Faction
from game.naming import namegen from game.naming import namegen
from game.scenery_group import SceneryGroup from game.scenery_group import SceneryGroup
from game.theater import PointWithHeading, PresetLocation from game.theater import PointWithHeading, PresetLocation, NavalControlPoint
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
IadsBuildingGroundObject, IadsBuildingGroundObject,
@ -30,8 +31,11 @@ from .theatergroup import IadsGroundGroup, IadsRole, SceneryUnit, TheaterGroup
from ..armedforces.armedforces import ArmedForces from ..armedforces.armedforces import ArmedForces
from ..armedforces.forcegroup import ForceGroup from ..armedforces.forcegroup import ForceGroup
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
from ..campaignloader.campaigncarrierconfig import CampaignCarrierConfig
from ..campaignloader.campaigngroundconfig import TgoConfig from ..campaignloader.campaigngroundconfig import TgoConfig
from ..data.groups import GroupTask from ..data.groups import GroupTask
from ..data.units import UnitClass
from ..dcs.shipunittype import ShipUnitType
from ..profiling import logged_duration from ..profiling import logged_duration
from ..settings import Settings from ..settings import Settings
@ -49,6 +53,7 @@ class GeneratorSettings:
no_player_navy: bool no_player_navy: bool
no_enemy_navy: bool no_enemy_navy: bool
tgo_config: TgoConfig tgo_config: TgoConfig
carrier_config: CampaignCarrierConfig
squadrons_start_full: bool squadrons_start_full: bool
@ -127,11 +132,13 @@ class GameGenerator:
def should_remove_carrier(self, player: bool) -> bool: def should_remove_carrier(self, player: bool) -> bool:
faction = self.player if player else self.enemy 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: def should_remove_lha(self, player: bool) -> bool:
faction = self.player if player else self.enemy faction = self.player if player else self.enemy
return self.generator_settings.no_lha or not faction.helicopter_carrier_names return self.generator_settings.no_lha or not [
x for x in faction.carriers if x.unit_class == UnitClass.HELICOPTER_CARRIER
]
def prepare_theater(self) -> None: def prepare_theater(self) -> None:
to_remove: List[ControlPoint] = [] to_remove: List[ControlPoint] = []
@ -223,14 +230,52 @@ class GenericCarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
carrier = next(self.control_point.ground_objects[-1].units) carrier = next(self.control_point.ground_objects[-1].units)
carrier.name = carrier_name 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]
if preferred_type and preferred_type.dcs_unit_type in [
v
for k, v in country_dict[self.faction.country.id].Ship.__dict__.items() # type: ignore
if "__" not in k
]:
carrier_unit.type = preferred_type.dcs_unit_type
if preferred_name:
self.control_point.name = preferred_name
else:
carrier_type = preferred_type if preferred_type else carrier_unit.unit_type
assert isinstance(carrier_type, ShipUnitType)
# 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
carrier_unit.name = self.control_point.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): class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
def generate(self) -> bool: def generate(self) -> bool:
if not super().generate(): if not super().generate():
return False return False
carrier_names = self.faction.carrier_names carriers = self.faction.carriers
if not carrier_names: if not carriers:
logging.info( logging.info(
f"Skipping generation of {self.control_point.name} because " f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no carriers" f"{self.faction_name} has no carriers"
@ -241,6 +286,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
if not unit_group: if not unit_group:
logging.error(f"{self.faction_name} has no access to AircraftCarrier") logging.error(f"{self.faction_name} has no access to AircraftCarrier")
return False return False
self.generate_ground_object_from_group( self.generate_ground_object_from_group(
unit_group, unit_group,
PresetLocation( PresetLocation(
@ -250,7 +296,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
), ),
GroupTask.AIRCRAFT_CARRIER, GroupTask.AIRCRAFT_CARRIER,
) )
self.update_carrier_name(random.choice(list(carrier_names))) self.apply_carrier_config()
return True return True
@ -259,8 +305,8 @@ class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
if not super().generate(): if not super().generate():
return False return False
lha_names = self.faction.helicopter_carrier_names lhas = self.faction.carriers
if not lha_names: if not lhas:
logging.info( logging.info(
f"Skipping generation of {self.control_point.name} because " f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no LHAs" f"{self.faction_name} has no LHAs"
@ -282,7 +328,7 @@ class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
), ),
GroupTask.HELICOPTER_CARRIER, GroupTask.HELICOPTER_CARRIER,
) )
self.update_carrier_name(random.choice(list(lha_names))) self.apply_carrier_config()
return True return True

View File

@ -318,6 +318,7 @@ def create_game(
no_player_navy=False, no_player_navy=False,
no_enemy_navy=False, no_enemy_navy=False,
tgo_config=campaign.load_ground_forces_config(), tgo_config=campaign.load_ground_forces_config(),
carrier_config=campaign.load_carrier_config(),
), ),
ModSettings( ModSettings(
a4_skyhawk=False, a4_skyhawk=False,

View File

@ -84,6 +84,7 @@ class NewGameWizard(QtWidgets.QWizard):
no_player_navy=self.field("no_player_navy"), no_player_navy=self.field("no_player_navy"),
no_enemy_navy=self.field("no_enemy_navy"), no_enemy_navy=self.field("no_enemy_navy"),
tgo_config=campaign.load_ground_forces_config(), tgo_config=campaign.load_ground_forces_config(),
carrier_config=campaign.load_carrier_config(),
squadrons_start_full=self.field("squadrons_start_full"), squadrons_start_full=self.field("squadrons_start_full"),
) )
mod_settings = ModSettings( mod_settings = ModSettings(

View File

@ -0,0 +1,4 @@
class: AircraftCarrier
price: 0
variants:
CVN-71 Theodore Roosevelt: null

View File

@ -0,0 +1,4 @@
class: AircraftCarrier
price: 0
variants:
CVN-72 Abraham Lincoln: null

View File

@ -0,0 +1,4 @@
class: AircraftCarrier
price: 0
variants:
CVN-73 George Washington: null

View File

@ -0,0 +1,4 @@
class: AircraftCarrier
price: 0
variants:
CVN-75 Harry S. Truman: null

View File

@ -0,0 +1,4 @@
class: AircraftCarrier
price: 0
variants:
CV 1143.5 Admiral Kuznetsov (2017): null

View File

@ -92,13 +92,13 @@ class TestFactionLoader(unittest.TestCase):
self.assertIn(Infantry.Soldier_M249, faction.infantry_units) self.assertIn(Infantry.Soldier_M249, faction.infantry_units)
self.assertIn(Stennis.name, faction.naval_units) self.assertIn(Stennis.name, faction.naval_units)
self.assertIn(Stennis, faction.carriers.keys())
self.assertIn(LHA_Tarawa.name, faction.naval_units) self.assertIn(LHA_Tarawa.name, faction.naval_units)
self.assertIn("mod", faction.requirements.keys()) self.assertIn("mod", faction.requirements.keys())
self.assertIn("Some mod is required", faction.requirements.values()) 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") @pytest.mark.skip(reason="Faction unit names in the json files are outdated")
def test_load_valid_faction_with_invalid_country(self) -> None: def test_load_valid_faction_with_invalid_country(self) -> None: