mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Configurable carriers
This commit is contained in:
parent
e416e07366
commit
d2fd7bbb4e
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
42
game/campaignloader/campaigncarrierconfig.py
Normal file
42
game/campaignloader/campaigncarrierconfig.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -1395,6 +1395,11 @@ class NavalControlPoint(
|
||||
L02,
|
||||
L52,
|
||||
L61,
|
||||
CV_1143_5,
|
||||
CVN_71,
|
||||
CVN_72,
|
||||
CVN_73,
|
||||
CVN_75,
|
||||
]:
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
4
resources/units/ships/CVN_71.yaml
Normal file
4
resources/units/ships/CVN_71.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
class: AircraftCarrier
|
||||
price: 0
|
||||
variants:
|
||||
CVN-71 Theodore Roosevelt: null
|
||||
4
resources/units/ships/CVN_72.yaml
Normal file
4
resources/units/ships/CVN_72.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
class: AircraftCarrier
|
||||
price: 0
|
||||
variants:
|
||||
CVN-72 Abraham Lincoln: null
|
||||
4
resources/units/ships/CVN_73.yaml
Normal file
4
resources/units/ships/CVN_73.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
class: AircraftCarrier
|
||||
price: 0
|
||||
variants:
|
||||
CVN-73 George Washington: null
|
||||
4
resources/units/ships/CVN_75.yaml
Normal file
4
resources/units/ships/CVN_75.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
class: AircraftCarrier
|
||||
price: 0
|
||||
variants:
|
||||
CVN-75 Harry S. Truman: null
|
||||
4
resources/units/ships/CV_1143_5.yaml
Normal file
4
resources/units/ships/CV_1143_5.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
class: AircraftCarrier
|
||||
price: 0
|
||||
variants:
|
||||
CV 1143.5 Admiral Kuznetsov (2017): null
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user