mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Decoupling and generalization of templates
Improvement for factions and templates which will allow decoupling of the templates from the actual units - Implement UnitGroup class which matches unit_types and possible templates as the needed abstraction layer for decoupling. - Refactor UnitType, Add ShipUnitType and all ships we currently use - Remove serialized template.json and migrated to multiple yaml templates (one for each template) and multiple .miz - Reorganized a lot of templates and started with generalization of many types (AAA, Flak, SHORAD, Navy) - Fixed a lot of bugs from the previous reworks (group name generation, strike targets...) - Reorganized the faction file completly. removed redundant lists, added presets for complex groups / families of units like sams - Reworked the building template handling. Some templates are unused like "village" - Reworked how groups from templates can be merged again for the dcs group creation (e.g. the skynet plugin requires them to be in the same group) - Allow to define alternative tasks
This commit is contained in:
@@ -3,12 +3,13 @@ from __future__ import annotations
|
||||
import copy
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
|
||||
|
||||
import dcs
|
||||
from dcs.countries import country_dict
|
||||
from dcs.unittype import ShipType, UnitType
|
||||
from dcs.unittype import ShipType
|
||||
|
||||
from game.data.building_data import (
|
||||
WW2_ALLIES_BUILDINGS,
|
||||
@@ -22,10 +23,19 @@ from game.data.doctrine import (
|
||||
COLDWAR_DOCTRINE,
|
||||
WWII_DOCTRINE,
|
||||
)
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.data.units import UnitClass
|
||||
from game.data.groups import GroupRole, GroupTask
|
||||
from game import db
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from gen.templates import GroundObjectTemplates, TemplateCategory
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.dcs.unitgroup import UnitGroup
|
||||
from game.dcs.unittype import UnitType
|
||||
from gen.templates import (
|
||||
GroundObjectTemplates,
|
||||
GroundObjectTemplate,
|
||||
GroupTemplate,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater.start_generator import ModSettings
|
||||
@@ -70,50 +80,26 @@ class Faction:
|
||||
# Logistics units used
|
||||
logistics_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Possible SAMS site generators for this faction
|
||||
air_defenses: List[str] = field(default_factory=list)
|
||||
# Possible Air Defence units, Like EWRs
|
||||
air_defense_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Possible EWR generators for this faction.
|
||||
ewrs: List[GroundUnitType] = field(default_factory=list)
|
||||
# A list of all supported sets of units
|
||||
preset_groups: list[UnitGroup] = field(default_factory=list)
|
||||
|
||||
# Possible Missile site generators for this faction
|
||||
missiles: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible costal site generators for this faction
|
||||
coastal_defenses: List[str] = field(default_factory=list)
|
||||
missiles: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Required mods or asset packs
|
||||
requirements: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# possible aircraft carrier units
|
||||
aircraft_carrier: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# possible helicopter carrier units
|
||||
helicopter_carrier: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# Possible carrier names
|
||||
carrier_names: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible helicopter carrier names
|
||||
helicopter_carrier_names: List[str] = field(default_factory=list)
|
||||
|
||||
# Navy group generators
|
||||
navy_generators: List[str] = field(default_factory=list)
|
||||
|
||||
# Available destroyers
|
||||
destroyers: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# Available cruisers
|
||||
cruisers: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# How many navy group should we try to generate per CP on startup for this faction
|
||||
navy_group_count: int = field(default=1)
|
||||
|
||||
# How many missiles group should we try to generate per CP on startup for this faction
|
||||
missiles_group_count: int = field(default=1)
|
||||
|
||||
# How many coastal group should we try to generate per CP on startup for this faction
|
||||
coastal_group_count: int = field(default=1)
|
||||
# Available Naval Units
|
||||
naval_units: List[ShipUnitType] = field(default_factory=list)
|
||||
|
||||
# Whether this faction has JTAC access
|
||||
has_jtac: bool = field(default=False)
|
||||
@@ -124,7 +110,7 @@ class Faction:
|
||||
# doctrine
|
||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||
|
||||
# List of available buildings for this faction
|
||||
# List of available building templates for this faction
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# List of default livery overrides
|
||||
@@ -142,64 +128,183 @@ class Faction:
|
||||
# All possible templates which can be generated by the faction
|
||||
templates: GroundObjectTemplates = field(default=GroundObjectTemplates())
|
||||
|
||||
# All available unit_groups
|
||||
unit_groups: dict[GroupRole, list[UnitGroup]] = field(default_factory=dict)
|
||||
|
||||
# Save all accessible units for performance increase
|
||||
_accessible_units: list[UnitType[Any]] = field(default_factory=list)
|
||||
|
||||
def __getitem__(self, item: str) -> Any:
|
||||
return getattr(self, item)
|
||||
|
||||
def has_access_to_unit_type(self, unit_type: str) -> bool:
|
||||
# Supports all GroundUnit lists and AirDefenses
|
||||
for unit in self.ground_units:
|
||||
if unit_type == unit.dcs_id:
|
||||
return True
|
||||
return unit_type in self.air_defenses
|
||||
@property
|
||||
def accessible_units(self) -> Iterator[UnitType[Any]]:
|
||||
yield from self._accessible_units
|
||||
|
||||
def has_access_to_unit_class(self, unit_class: GroundUnitClass) -> bool:
|
||||
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
|
||||
if vehicle.unit_class is unit_class:
|
||||
return True
|
||||
@property
|
||||
def air_defenses(self) -> list[str]:
|
||||
"""Returns the Air Defense types"""
|
||||
air_defenses = [a.name for a in self.air_defense_units]
|
||||
air_defenses.extend(
|
||||
[pg.name for pg in self.preset_groups if pg.role == GroupRole.AntiAir]
|
||||
)
|
||||
return sorted(air_defenses)
|
||||
|
||||
def has_access_to_unit_type(self, unit_type: str) -> bool:
|
||||
# GroundUnits
|
||||
if any(unit_type == u.dcs_id for u in self.accessible_units):
|
||||
return True
|
||||
|
||||
# Statics
|
||||
if db.static_type_from_name(unit_type) is not None:
|
||||
# TODO Improve the statics checking
|
||||
return True
|
||||
return False
|
||||
|
||||
def load_templates(self, all_templates: GroundObjectTemplates) -> None:
|
||||
# This loads all faction possible sam templates and the default ones
|
||||
# For legacy reasons this allows to check for template names. This can be
|
||||
# improved in the future to have more control about the possible Templates.
|
||||
# For example it can be possible to define the unit_types and check if all
|
||||
# requirements for the template are fulfilled.
|
||||
for category, template in all_templates.templates:
|
||||
if (
|
||||
(
|
||||
category == TemplateCategory.AirDefence
|
||||
and (
|
||||
# Check if faction has the template name or ALL required
|
||||
# unit_types in the list air_defenses. For legacy reasons this
|
||||
# allows both and also the EWR template
|
||||
template.name in self.air_defenses
|
||||
or all(
|
||||
self.has_access_to_unit_type(required_unit)
|
||||
for required_unit in template.required_units
|
||||
)
|
||||
or template.template_type == "EWR"
|
||||
)
|
||||
)
|
||||
or template.name in self.navy_generators
|
||||
or template.name in self.missiles
|
||||
or template.name in self.coastal_defenses
|
||||
or (
|
||||
template.template_type
|
||||
in self.building_set + ["fob", "ammo", "factory"]
|
||||
)
|
||||
or (template.template_type == "carrier" and self.aircraft_carrier)
|
||||
or (template.template_type == "lha" and self.helicopter_carrier)
|
||||
or category == TemplateCategory.Armor
|
||||
):
|
||||
# Make a deep copy of a template and add it to the template_list.
|
||||
# This is required to have faction independent templates. Otherwise
|
||||
# the reference would be the same and changes would affect all.
|
||||
faction_template = copy.deepcopy(template)
|
||||
# Initialize all randomizers
|
||||
for group_template in faction_template.groups:
|
||||
if group_template.randomizer:
|
||||
group_template.randomizer.init_randomization_for_faction(self)
|
||||
self.templates.add_template(category, faction_template)
|
||||
def has_access_to_unit_class(self, unit_class: UnitClass) -> bool:
|
||||
return any(unit.unit_class is unit_class for unit in self.accessible_units)
|
||||
|
||||
def _load_accessible_units(self, templates: GroundObjectTemplates) -> None:
|
||||
self._accessible_units = []
|
||||
all_units: Iterator[UnitType[Any]] = itertools.chain(
|
||||
self.ground_units,
|
||||
self.infantry_units,
|
||||
self.air_defense_units,
|
||||
self.naval_units,
|
||||
self.missiles,
|
||||
(
|
||||
ground_unit
|
||||
for preset_group in self.preset_groups
|
||||
for ground_unit in preset_group.ground_units
|
||||
),
|
||||
(
|
||||
ship_unit
|
||||
for preset_group in self.preset_groups
|
||||
for ship_unit in preset_group.ship_units
|
||||
),
|
||||
)
|
||||
for unit in all_units:
|
||||
if unit not in self._accessible_units:
|
||||
self._accessible_units.append(unit)
|
||||
|
||||
def initialize(
|
||||
self, all_templates: GroundObjectTemplates, mod_settings: ModSettings
|
||||
) -> None:
|
||||
# Apply the mod settings
|
||||
self._apply_mod_settings(mod_settings)
|
||||
# Load all accessible units and store them for performant later usage
|
||||
self._load_accessible_units(all_templates)
|
||||
# Load all faction compatible templates
|
||||
self._load_templates(all_templates)
|
||||
# Load Unit Groups
|
||||
self._load_unit_groups()
|
||||
|
||||
def _add_unit_group(self, unit_group: UnitGroup, merge: bool = True) -> None:
|
||||
if not unit_group.templates:
|
||||
unit_group.load_templates(self)
|
||||
if not unit_group.templates:
|
||||
# Empty templates will throw an error on generation
|
||||
logging.error(
|
||||
f"Skipping Unit group {unit_group.name} as no templates are available to generate the group"
|
||||
)
|
||||
return
|
||||
if unit_group.role in self.unit_groups:
|
||||
for group in self.unit_groups[unit_group.role]:
|
||||
if merge and all(task in group.tasks for task in unit_group.tasks):
|
||||
# Update existing group if same tasking
|
||||
group.update_from_unit_group(unit_group)
|
||||
return
|
||||
# Add new Unit_group
|
||||
self.unit_groups[unit_group.role].append(unit_group)
|
||||
else:
|
||||
self.unit_groups[unit_group.role] = [unit_group]
|
||||
|
||||
def _load_unit_groups(self) -> None:
|
||||
# This function will create all the UnitGroups for the faction
|
||||
# It will create a unit group for each global Template, Building or
|
||||
# Legacy supported templates (not yet migrated from the generators).
|
||||
# For every preset_group there will be a separate UnitGroup so no mixed
|
||||
# UnitGroups will be generated for them. Special groups like complex SAM Systems
|
||||
self.unit_groups = {}
|
||||
|
||||
# Generate UnitGroups for all global templates
|
||||
for role, template in self.templates.templates:
|
||||
if template.generic or role == GroupRole.Building:
|
||||
# Build groups for global templates and buildings
|
||||
self._add_group_for_template(role, template)
|
||||
|
||||
# Add preset groups
|
||||
for preset_group in self.preset_groups:
|
||||
# Add as separate group, do not merge with generic groups!
|
||||
self._add_unit_group(preset_group, False)
|
||||
|
||||
def _add_group_for_template(
|
||||
self, role: GroupRole, template: GroundObjectTemplate
|
||||
) -> None:
|
||||
unit_group = UnitGroup(
|
||||
f"{role.value}: {', '.join([t.value for t in template.tasks])}",
|
||||
[u for u in template.units if isinstance(u, GroundUnitType)],
|
||||
[u for u in template.units if isinstance(u, ShipUnitType)],
|
||||
list(template.statics),
|
||||
role,
|
||||
)
|
||||
unit_group.tasks = template.tasks
|
||||
unit_group.set_templates([template])
|
||||
self._add_unit_group(unit_group)
|
||||
|
||||
def initialize_group_template(
|
||||
self, group: GroupTemplate, faction_sensitive: bool = True
|
||||
) -> bool:
|
||||
# Sensitive defines if the initialization should check if the unit is available
|
||||
# to this faction or not. It is disabled for migration only atm.
|
||||
unit_types = [
|
||||
t
|
||||
for t in group.unit_types
|
||||
if not faction_sensitive or self.has_access_to_unit_type(t)
|
||||
]
|
||||
|
||||
alternative_types = []
|
||||
for accessible_unit in self.accessible_units:
|
||||
if accessible_unit.unit_class in group.unit_classes:
|
||||
unit_types.append(accessible_unit.dcs_id)
|
||||
if accessible_unit.unit_class in group.alternative_classes:
|
||||
alternative_types.append(accessible_unit.dcs_id)
|
||||
|
||||
if not unit_types and not alternative_types and not group.optional:
|
||||
raise StopIteration
|
||||
|
||||
types = unit_types or alternative_types
|
||||
group.set_possible_types(types)
|
||||
return len(types) > 0
|
||||
|
||||
def _load_templates(self, all_templates: GroundObjectTemplates) -> None:
|
||||
self.templates = GroundObjectTemplates()
|
||||
# This loads all templates which are usable by the faction
|
||||
for role, template in all_templates.templates:
|
||||
# Make a deep copy of a template and add it to the template_list.
|
||||
# This is required to have faction independent templates. Otherwise
|
||||
# the reference would be the same and changes would affect all.
|
||||
faction_template = copy.deepcopy(template)
|
||||
try:
|
||||
faction_template.groups[:] = [
|
||||
group_template
|
||||
for group_template in faction_template.groups
|
||||
if self.initialize_group_template(group_template)
|
||||
]
|
||||
if (
|
||||
role == GroupRole.Building
|
||||
and GroupTask.StrikeTarget in template.tasks
|
||||
and faction_template.category not in self.building_set
|
||||
):
|
||||
# Special handling for strike targets. Skip if not supported by faction
|
||||
continue
|
||||
if faction_template.groups:
|
||||
self.templates.add_template(role, faction_template)
|
||||
continue
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
logging.info(f"{self.name} can not use template {template.name}")
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
@@ -240,34 +345,30 @@ class Faction:
|
||||
faction.logistics_units = [
|
||||
GroundUnitType.named(n) for n in json.get("logistics_units", [])
|
||||
]
|
||||
faction.ewrs = [GroundUnitType.named(n) for n in json.get("ewrs", [])]
|
||||
faction.air_defense_units = [
|
||||
GroundUnitType.named(n) for n in json.get("air_defense_units", [])
|
||||
]
|
||||
faction.missiles = [GroundUnitType.named(n) for n in json.get("missiles", [])]
|
||||
|
||||
faction.air_defenses = json.get("air_defenses", [])
|
||||
# Compatibility for older factions. All air defenses now belong to a
|
||||
# single group and the generator decides what belongs where.
|
||||
faction.air_defenses.extend(json.get("sams", []))
|
||||
faction.air_defenses.extend(json.get("shorads", []))
|
||||
faction.preset_groups = [
|
||||
UnitGroup.named(n) for n in json.get("preset_groups", [])
|
||||
]
|
||||
|
||||
faction.missiles = json.get("missiles", [])
|
||||
faction.coastal_defenses = json.get("coastal_defenses", [])
|
||||
faction.requirements = json.get("requirements", {})
|
||||
|
||||
faction.carrier_names = json.get("carrier_names", [])
|
||||
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
|
||||
faction.navy_generators = json.get("navy_generators", [])
|
||||
faction.aircraft_carrier = load_all_ships(json.get("aircraft_carrier", []))
|
||||
faction.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", []))
|
||||
faction.destroyers = load_all_ships(json.get("destroyers", []))
|
||||
faction.cruisers = load_all_ships(json.get("cruisers", []))
|
||||
|
||||
faction.naval_units = [
|
||||
ShipUnitType.named(n) for n in json.get("naval_units", [])
|
||||
]
|
||||
|
||||
faction.has_jtac = json.get("has_jtac", False)
|
||||
jtac_name = json.get("jtac_unit", None)
|
||||
if jtac_name is not None:
|
||||
faction.jtac_unit = AircraftType.named(jtac_name)
|
||||
else:
|
||||
faction.jtac_unit = None
|
||||
faction.navy_group_count = int(json.get("navy_group_count", 1))
|
||||
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
|
||||
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
|
||||
|
||||
# Load doctrine
|
||||
doctrine = json.get("doctrine", "modern")
|
||||
@@ -304,6 +405,7 @@ class Faction:
|
||||
|
||||
# Templates
|
||||
faction.templates = GroundObjectTemplates()
|
||||
|
||||
return faction
|
||||
|
||||
@property
|
||||
@@ -312,14 +414,49 @@ class Faction:
|
||||
yield from self.frontline_units
|
||||
yield from self.logistics_units
|
||||
|
||||
def infantry_with_class(
|
||||
self, unit_class: GroundUnitClass
|
||||
) -> Iterator[GroundUnitType]:
|
||||
def infantry_with_class(self, unit_class: UnitClass) -> Iterator[GroundUnitType]:
|
||||
for unit in self.infantry_units:
|
||||
if unit.unit_class is unit_class:
|
||||
yield unit
|
||||
|
||||
def apply_mod_settings(self, mod_settings: ModSettings) -> Faction:
|
||||
def groups_for_role_and_task(
|
||||
self, group_role: GroupRole, group_task: Optional[GroupTask] = None
|
||||
) -> list[UnitGroup]:
|
||||
if group_role not in self.unit_groups:
|
||||
return []
|
||||
groups = []
|
||||
for unit_group in self.unit_groups[group_role]:
|
||||
if not group_task or group_task in unit_group.tasks:
|
||||
groups.append(unit_group)
|
||||
return groups
|
||||
|
||||
def groups_for_role_and_tasks(
|
||||
self, group_role: GroupRole, tasks: list[GroupTask]
|
||||
) -> list[UnitGroup]:
|
||||
groups = []
|
||||
for task in tasks:
|
||||
for group in self.groups_for_role_and_task(group_role, task):
|
||||
if group not in groups:
|
||||
groups.append(group)
|
||||
return groups
|
||||
|
||||
def random_group_for_role(self, group_role: GroupRole) -> Optional[UnitGroup]:
|
||||
unit_groups = self.groups_for_role_and_task(group_role)
|
||||
return random.choice(unit_groups) if unit_groups else None
|
||||
|
||||
def random_group_for_role_and_task(
|
||||
self, group_role: GroupRole, group_task: GroupTask
|
||||
) -> Optional[UnitGroup]:
|
||||
unit_groups = self.groups_for_role_and_task(group_role, group_task)
|
||||
return random.choice(unit_groups) if unit_groups else None
|
||||
|
||||
def random_group_for_role_and_tasks(
|
||||
self, group_role: GroupRole, tasks: list[GroupTask]
|
||||
) -> Optional[UnitGroup]:
|
||||
unit_groups = self.groups_for_role_and_tasks(group_role, tasks)
|
||||
return random.choice(unit_groups) if unit_groups else None
|
||||
|
||||
def _apply_mod_settings(self, mod_settings: ModSettings) -> None:
|
||||
# aircraft
|
||||
if not mod_settings.a4_skyhawk:
|
||||
self.remove_aircraft("A-4E-C")
|
||||
@@ -379,24 +516,23 @@ class Faction:
|
||||
self.remove_vehicle("KORNET")
|
||||
# high digit sams
|
||||
if not mod_settings.high_digit_sams:
|
||||
self.remove_air_defenses("SA-10B/S-300PS Battery")
|
||||
self.remove_air_defenses("SA-12/S-300V Battery")
|
||||
self.remove_air_defenses("SA-20/S-300PMU-1 Battery")
|
||||
self.remove_air_defenses("SA-20B/S-300PMU-2 Battery")
|
||||
self.remove_air_defenses("SA-23/S-300VM Battery")
|
||||
self.remove_air_defenses("SA-17 Grizzly Battery")
|
||||
self.remove_air_defenses("KS-19 AAA Site")
|
||||
return self
|
||||
self.remove_presets("SA-10B/S-300PS")
|
||||
self.remove_presets("SA-12/S-300V")
|
||||
self.remove_presets("SA-20/S-300PMU-1")
|
||||
self.remove_presets("SA-20B/S-300PMU-2")
|
||||
self.remove_presets("SA-23/S-300VM")
|
||||
self.remove_presets("SA-17")
|
||||
self.remove_presets("KS-19")
|
||||
|
||||
def remove_aircraft(self, name: str) -> None:
|
||||
for i in self.aircrafts:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.aircrafts.remove(i)
|
||||
|
||||
def remove_air_defenses(self, name: str) -> None:
|
||||
for i in self.air_defenses:
|
||||
if i == name:
|
||||
self.air_defenses.remove(i)
|
||||
def remove_presets(self, name: str) -> None:
|
||||
for pg in self.preset_groups:
|
||||
if pg.name == name:
|
||||
self.preset_groups.remove(pg)
|
||||
|
||||
def remove_vehicle(self, name: str) -> None:
|
||||
for i in self.frontline_units:
|
||||
|
||||
Reference in New Issue
Block a user