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
* **[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 Design]** Ability to configure specific carrier names & types in campaign's yaml file
## Fixes
* **[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.version import CAMPAIGN_FORMAT_VERSION
from .campaignairwingconfig import CampaignAirWingConfig
from .campaigncarrierconfig import CampaignCarrierConfig
from .campaigngroundconfig import TgoConfig
from .mizcampaignloader import MizCampaignLoader
from ..factions import FACTIONS, Faction
@ -164,6 +165,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:

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 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,11 +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 helicopter carrier names
helicopter_carrier_names: Set[str] = field(default_factory=set)
# Possible carrier units mapped to names
carriers: Dict[ShipUnitType, Set[str]] = field(default_factory=dict)
# Available Naval Units
naval_units: Set[ShipUnitType] = field(default_factory=set)
@ -241,8 +239,30 @@ 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", [])
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)
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:
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

View File

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

View File

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

View File

@ -7,12 +7,13 @@ from datetime import datetime, time
from typing import List, Optional
import dcs.statics
from dcs.countries import country_dict
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 +31,11 @@ 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 ..data.units import UnitClass
from ..dcs.shipunittype import ShipUnitType
from ..profiling import logged_duration
from ..settings import Settings
@ -49,6 +53,7 @@ class GeneratorSettings:
no_player_navy: bool
no_enemy_navy: bool
tgo_config: TgoConfig
carrier_config: CampaignCarrierConfig
squadrons_start_full: bool
@ -127,11 +132,13 @@ 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
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:
to_remove: List[ControlPoint] = []
@ -223,14 +230,52 @@ 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]
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):
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"
@ -241,6 +286,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(
@ -250,7 +296,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
),
GroupTask.AIRCRAFT_CARRIER,
)
self.update_carrier_name(random.choice(list(carrier_names)))
self.apply_carrier_config()
return True
@ -259,8 +305,8 @@ class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
if not super().generate():
return False
lha_names = self.faction.helicopter_carrier_names
if not lha_names:
lhas = self.faction.carriers
if not lhas:
logging.info(
f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no LHAs"
@ -282,7 +328,7 @@ class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator):
),
GroupTask.HELICOPTER_CARRIER,
)
self.update_carrier_name(random.choice(list(lha_names)))
self.apply_carrier_config()
return True

View File

@ -318,6 +318,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,

View File

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

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(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(5, len(faction.helicopter_carrier_names))
self.assertEqual(4, len(faction.carriers))
@pytest.mark.skip(reason="Faction unit names in the json files are outdated")
def test_load_valid_faction_with_invalid_country(self) -> None: